diff --git a/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourceContent.java b/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourceContent.java new file mode 100644 index 00000000..3d383547 --- /dev/null +++ b/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourceContent.java @@ -0,0 +1,23 @@ +package com.vaadin.flow.portal.streamresource; + +import java.io.ByteArrayInputStream; + +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.server.StreamResource; + +public class StreamResourceContent extends VerticalLayout { + + static final String FILENAME = "export.xlsx"; + + public StreamResourceContent() { + StreamResource downloadResource = new StreamResource(FILENAME, + () -> new ByteArrayInputStream(new byte[0])); + downloadResource.setContentType("application/xls"); + downloadResource.setHeader("Content-Disposition", + "attachment;filename=export.xlsx"); + Anchor link = new Anchor(downloadResource, "Download File"); + link.setId("downloadLink"); + add(link); + } +} diff --git a/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourcePortlet.java b/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourcePortlet.java new file mode 100644 index 00000000..44262f99 --- /dev/null +++ b/vaadin-portlet-integration-tests/tests-generic/src/main/java/com/vaadin/flow/portal/streamresource/StreamResourcePortlet.java @@ -0,0 +1,7 @@ +package com.vaadin.flow.portal.streamresource; + +import com.vaadin.flow.portal.VaadinPortlet; + +public class StreamResourcePortlet + extends VaadinPortlet<StreamResourceContent> { +} diff --git a/vaadin-portlet-integration-tests/tests-generic/src/main/webapp/WEB-INF/portlet.xml b/vaadin-portlet-integration-tests/tests-generic/src/main/webapp/WEB-INF/portlet.xml index d7c1791a..32b89c02 100644 --- a/vaadin-portlet-integration-tests/tests-generic/src/main/webapp/WEB-INF/portlet.xml +++ b/vaadin-portlet-integration-tests/tests-generic/src/main/webapp/WEB-INF/portlet.xml @@ -74,4 +74,15 @@ </supports> </portlet> + <portlet> + <portlet-name>streamresource</portlet-name> + <display-name>Flow Portlet with StreamResource download</display-name> + <portlet-class>com.vaadin.flow.portal.streamresource.StreamResourcePortlet</portlet-class> + <expiration-cache>0</expiration-cache> + <supports> + <mime-type>text/html</mime-type> + <portlet-mode>VIEW</portlet-mode> + </supports> + </portlet> + </portlet-app> diff --git a/vaadin-portlet-integration-tests/tests-generic/src/test/java/com/vaadin/flow/portal/streamresource/StreamResourceIT.java b/vaadin-portlet-integration-tests/tests-generic/src/test/java/com/vaadin/flow/portal/streamresource/StreamResourceIT.java new file mode 100644 index 00000000..22446fdc --- /dev/null +++ b/vaadin-portlet-integration-tests/tests-generic/src/test/java/com/vaadin/flow/portal/streamresource/StreamResourceIT.java @@ -0,0 +1,68 @@ +package com.vaadin.flow.portal.streamresource; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.JavascriptExecutor; + +import com.vaadin.flow.component.html.testbench.AnchorElement; +import com.vaadin.flow.portal.AbstractPlutoPortalTest; + +public class StreamResourceIT extends AbstractPlutoPortalTest { + + public StreamResourceIT() { + super("tests-generic", "streamresource"); + } + + @Test + public void downloadStreamResource_responseHeadersAreSent() { + AnchorElement link = getVaadinPortletRootElement() + .$(AnchorElement.class).id("downloadLink"); + String url = link.getAttribute("href"); + getDriver().manage().timeouts().setScriptTimeout(15, TimeUnit.SECONDS); + + Map<String, String> headers = downloadAndGetResponseHeaders(url); + + Assert.assertEquals( + "attachment;filename=" + StreamResourceContent.FILENAME, + headers.getOrDefault("content-disposition", null)); + } + + /* + * Stolen from stackexchange. + * + * It's not possible to use a straight way to download the link externally + * since it will use another session and the link will be invalid in this + * session. So either this pure client side way or external download with + * cookies copy (which allows preserve the session) needs to be used. + */ + @SuppressWarnings("unchecked") + public Map<String, String> downloadAndGetResponseHeaders(String url) { + String script = "var url = arguments[0];" + + "var callback = arguments[arguments.length - 1];" + + "var xhr = new XMLHttpRequest();" + + "xhr.open('GET', url, true);" + + "xhr.responseType = \"arraybuffer\";" + + // force the HTTP response, response-type header to be array + // buffer + "xhr.onload = function() {" + // Get the raw header string " + + " var headers = xhr.getAllResponseHeaders();" + // Convert the header string into an array + // of individual headers + + " var arr = headers.trim().split(/[\\r\\n]+/);" + // Create a map of header names to values + + " var headerMap = {};" + " arr.forEach(function (line) { " + + " var parts = line.split(': '); " + + " var header = parts.shift().toLowerCase(); " + + " var value = parts.join(': '); " + + " headerMap[header] = value;" + " }); " + + " callback(headerMap);" + "};" + "xhr.send();"; + Object response = ((JavascriptExecutor) getDriver()) + .executeAsyncScript(script, url); + return (Map<String, String>) response; + } +} diff --git a/vaadin-portlet/src/main/java/com/vaadin/flow/portal/PortletStreamResourceHandler.java b/vaadin-portlet/src/main/java/com/vaadin/flow/portal/PortletStreamResourceHandler.java index 94747520..3c84afec 100644 --- a/vaadin-portlet/src/main/java/com/vaadin/flow/portal/PortletStreamResourceHandler.java +++ b/vaadin-portlet/src/main/java/com/vaadin/flow/portal/PortletStreamResourceHandler.java @@ -25,6 +25,7 @@ public void handleRequest(VaadinSession session, VaadinRequest request, try { setResponseContentType(request, response, streamResource); response.setCacheTime(streamResource.getCacheTime()); + streamResource.getHeaders().forEach(response::setHeader); writer = streamResource.getWriter(); if (writer == null) { throw new IOException( @@ -56,8 +57,7 @@ public void handleRequest(VaadinSession session, VaadinRequest request, } private void setResponseContentType(VaadinRequest request, - VaadinResponse response, - StreamResource streamResource) { + VaadinResponse response, StreamResource streamResource) { PortletContext context = ((VaadinPortletRequest) request) .getPortletContext(); try { @@ -65,8 +65,7 @@ private void setResponseContentType(VaadinRequest request, .apply(streamResource, null)); } catch (NullPointerException e) { response.setContentType(Optional - .ofNullable( - context.getMimeType(streamResource.getName())) + .ofNullable(context.getMimeType(streamResource.getName())) .orElse("application/octet-stream")); } } diff --git a/vaadin-portlet/src/test/java/com/vaadin/flow/portal/PortletStreamResourceHandlerTest.java b/vaadin-portlet/src/test/java/com/vaadin/flow/portal/PortletStreamResourceHandlerTest.java new file mode 100644 index 00000000..99f22d09 --- /dev/null +++ b/vaadin-portlet/src/test/java/com/vaadin/flow/portal/PortletStreamResourceHandlerTest.java @@ -0,0 +1,70 @@ +package com.vaadin.flow.portal; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.AbstractStreamResource; +import com.vaadin.flow.server.StreamResource; + +import static org.junit.Assert.*; + +public class PortletStreamResourceHandlerTest { + + private PortletStreamResourceHandler handler = new PortletStreamResourceHandler(); + private VaadinPortletSession session; + private VaadinPortletService service; + + @Before + public void init() { + service = Mockito.mock(VaadinPortletService.class); + session = new VaadinPortletSession(service) { + @Override + public boolean hasLock() { + return true; + } + + @Override + public void lock() { + } + + @Override + public void unlock() { + } + + @Override + public void checkHasLock() { + + } + }; + } + + @Test + public void handleRequest_shouldApplyStreamResourceHeaders() + throws IOException { + PortletRequest portletRequest = Mockito.mock(PortletRequest.class); + VaadinPortletRequest request = new VaadinPortletRequest(portletRequest, + service); + MimeResponse portletResponse = Mockito.mock(MimeResponse.class); + VaadinPortletResponse response = new VaadinPortletResponse( + portletResponse, service); + + StreamResource resource = new StreamResource("export.xlsx", + () -> new ByteArrayInputStream(new byte[0])); + resource.setContentType("application/xls"); + String headerName = "Content-Disposition"; + String headerValue = "attachment;filename=export.xlsx"; + resource.setHeader(headerName, headerValue); + handler.handleRequest(session, request, response, resource); + Mockito.verify(portletResponse, Mockito.atLeastOnce()).setProperty(headerName, headerValue); + } + +} \ No newline at end of file