diff options
author | Artur Signell <artur@vaadin.com> | 2012-10-23 13:09:10 +0000 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2012-10-23 13:09:10 +0000 |
commit | d8145ff765f88205210ee57f89b0445e1934cd56 (patch) | |
tree | 6b5d00cfe2f3583cafb9df68108ff4b2506aa1b1 | |
parent | 439b88b09de669189d71279e9a42588b5ee1a753 (diff) | |
parent | 055563a7166f4e3891929e3c8be5799789d68ae1 (diff) | |
download | vaadin-framework-d8145ff765f88205210ee57f89b0445e1934cd56.tar.gz vaadin-framework-d8145ff765f88205210ee57f89b0445e1934cd56.zip |
Merge "FileDownloader for starting downloads with any component (#9524)"
3 files changed, 366 insertions, 0 deletions
diff --git a/client/src/com/vaadin/client/extensions/FileDownloaderConnector.java b/client/src/com/vaadin/client/extensions/FileDownloaderConnector.java new file mode 100644 index 0000000000..d76efcc046 --- /dev/null +++ b/client/src/com/vaadin/client/extensions/FileDownloaderConnector.java @@ -0,0 +1,61 @@ +package com.vaadin.client.extensions; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.IFrameElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.server.FileDownloader; +import com.vaadin.shared.ui.Connect; + +@Connect(FileDownloader.class) +public class FileDownloaderConnector extends AbstractExtensionConnector + implements ClickHandler { + + private IFrameElement iframe; + + @Override + protected void extend(ServerConnector target) { + final Widget downloadWidget = ((ComponentConnector) target).getWidget(); + + downloadWidget.addDomHandler(this, ClickEvent.getType()); + } + + @Override + public void onClick(ClickEvent event) { + final String url = getResourceUrl("dl"); + if (url != null && !url.isEmpty()) { + if (iframe != null) { + // make sure it is not on dom tree already, might start + // multiple downloads at once + iframe.removeFromParent(); + } + iframe = Document.get().createIFrameElement(); + + Style style = iframe.getStyle(); + style.setVisibility(Visibility.HIDDEN); + style.setHeight(0, Unit.PX); + style.setWidth(0, Unit.PX); + + iframe.setFrameBorder(0); + iframe.setTabIndex(-1); + iframe.setSrc(url); + RootPanel.getBodyElement().appendChild(iframe); + } + } + + @Override + public void setParent(ServerConnector parent) { + super.setParent(parent); + if (parent == null) { + iframe.removeFromParent(); + } + } + +} diff --git a/server/src/com/vaadin/server/FileDownloader.java b/server/src/com/vaadin/server/FileDownloader.java new file mode 100644 index 0000000000..a07c0d3ed1 --- /dev/null +++ b/server/src/com/vaadin/server/FileDownloader.java @@ -0,0 +1,127 @@ +package com.vaadin.server; + +import java.io.IOException; + +import com.vaadin.ui.AbstractComponent; + +/** + * Extension that starts a download when the extended component is clicked. This + * is used to overcome two challenges: + * <ul> + * <li>Resource should be bound to a component to allow it to be garbage + * collected when there are no longer any ways of reaching the resource.</li> + * <li>Download should be started directly when the user clicks e.g. a Button + * without going through a server-side click listener to avoid triggering + * security warnings in some browsers.</li> + * </ul> + * <p> + * Please note that the download will be started in an iframe, which means that + * care should be taken to avoid serving content types that might make the + * browser attempt to show the content using a plugin instead of downloading it. + * Connector resources (e.g. {@link FileResource} and {@link ClassResource}) + * will automatically be served using a + * <code>Content-Type: application/octet-stream</code> header unless + * {@link #setOverrideContentType(boolean)} has been set to <code>false</code> + * while files served in other ways, (e.g. {@link ExternalResource} or + * {@link ThemeResource}) will not automatically get this treatment. + * </p> + * + * @author Vaadin Ltd + * @since 7.0.0 + */ +public class FileDownloader extends AbstractExtension { + + private boolean overrideContentType = true; + + /** + * Creates a new file downloader for the given resource. To use the + * downloader, you should also {@link #extend(AbstractClientConnector)} the + * component. + * + * @param resource + * the resource to download when the user clicks the extended + * component. + */ + public FileDownloader(Resource resource) { + if (resource == null) { + throw new IllegalArgumentException("resource may not be null"); + } + setResource("dl", resource); + } + + public void extend(AbstractComponent target) { + super.extend(target); + } + + /** + * Gets the resource set for download. + * + * @return the resource that will be downloaded if clicking the extended + * component + */ + public Resource getFileDownloadResource() { + return getResource("dl"); + } + + /** + * Sets whether the content type of served resources should be overriden to + * <code>application/octet-stream</code> to reduce the risk of a browser + * plugin choosing to display the resource instead of downloading it. This + * is by default set to <code>true</code>. + * <p> + * Please note that this only affects Connector resources (e.g. + * {@link FileResource} and {@link ClassResource}) but not other resource + * types (e.g. {@link ExternalResource} or {@link ThemeResource}). + * </p> + * + * @param overrideContentType + * <code>true</code> to override the content type if possible; + * <code>false</code> to use the original content type. + */ + public void setOverrideContentType(boolean overrideContentType) { + this.overrideContentType = overrideContentType; + } + + /** + * Checks whether the content type should be overridden. + * + * @see #setOverrideContentType(boolean) + * + * @return <code>true</code> if the content type will be overridden when + * possible; <code>false</code> if the original content type will be + * used. + */ + public boolean isOverrideContentType() { + return overrideContentType; + } + + @Override + public boolean handleConnectorRequest(VaadinRequest request, + VaadinResponse response, String path) throws IOException { + if (!path.matches("dl(/.*)?")) { + // Ignore if it isn't for us + return false; + } + + Resource resource = getFileDownloadResource(); + if (resource instanceof ConnectorResource) { + DownloadStream stream = ((ConnectorResource) resource).getStream(); + + if (stream.getParameter("Content-Disposition") == null) { + // Content-Disposition: attachment generally forces download + stream.setParameter("Content-Disposition", + "attachment; filename=\"" + stream.getFileName() + "\""); + } + + // Content-Type to block eager browser plug-ins from hijacking the + // file + if (isOverrideContentType()) { + stream.setContentType("application/octet-stream;charset=UTF-8"); + } + stream.writeResponse(request, response); + return true; + } else { + return false; + } + } +} diff --git a/uitest/src/com/vaadin/tests/components/FileDownloaderTest.java b/uitest/src/com/vaadin/tests/components/FileDownloaderTest.java new file mode 100644 index 0000000000..d5f447c7c3 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/FileDownloaderTest.java @@ -0,0 +1,178 @@ +package com.vaadin.tests.components; + +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +import com.vaadin.server.ClassResource; +import com.vaadin.server.ConnectorResource; +import com.vaadin.server.FileDownloader; +import com.vaadin.server.FileResource; +import com.vaadin.server.StreamResource; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.tests.components.embedded.EmbeddedPdf; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Component; +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.NativeButton; + +public class FileDownloaderTest extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + List<Class<? extends Component>> components = new ArrayList<Class<? extends Component>>(); + components.add(Button.class); + components.add(NativeButton.class); + components.add(CssLayout.class); + components.add(Label.class); + + // Resource resource = new ExternalResource( + // "https://vaadin.com/download/prerelease/7.0/7.0.0/7.0.0.beta1/vaadin-all-7.0.0.beta1.zip"); + // addComponents(resource, components); + // resource = new ExternalResource( + // "https://vaadin.com/download/book-of-vaadin/current/pdf/book-of-vaadin.pdf"); + // addComponents(resource, components); + ConnectorResource resource; + resource = new StreamResource(new StreamResource.StreamSource() { + + @Override + public InputStream getStream() { + try { + BufferedImage img = getImage2("demo.png"); + ByteArrayOutputStream imagebuffer = new ByteArrayOutputStream(); + ImageIO.write(img, "png", imagebuffer); + Thread.sleep(5000); + + return new ByteArrayInputStream(imagebuffer.toByteArray()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }, "demo.png"); + addComponents("Dynamic image", resource, components); + try { + File hugeFile = File.createTempFile("huge", ".txt"); + hugeFile.deleteOnExit(); + BufferedOutputStream os = new BufferedOutputStream( + new FileOutputStream(hugeFile)); + int writeAtOnce = 1024 * 1024; + byte[] b = new byte[writeAtOnce]; + for (int i = 0; i < 5l * 1024l * 1024l; i += writeAtOnce) { + os.write(b); + } + os.close(); + resource = new FileResource(hugeFile); + addComponents("Huge text file", resource, components); + } catch (IOException e) { + e.printStackTrace(); + } + // resource = new DynamicConnectorResource(this, "requestImage.png"); + // addComponents(resource, components); + // resource = new ThemeResource("favicon.ico"); + // addComponents(resource, components); + resource = new ClassResource(new EmbeddedPdf().getClass(), "test.pdf"); + addComponents("Class resource pdf", resource, components); + } + + public void addComponents(String caption, ConnectorResource resource, + List<Class<? extends Component>> components) { + HorizontalLayout layout = new HorizontalLayout(); + layout.setCaption(caption); + for (Class<? extends Component> cls : components) { + try { + AbstractComponent c = (AbstractComponent) cls.newInstance(); + c.setId(cls.getName()); + c.setCaption(cls.getName()); + c.setDescription(resource.getMIMEType() + " / " + + resource.getClass()); + c.setWidth("100px"); + c.setHeight("100px"); + + layout.addComponent(c); + + new FileDownloader(resource).extend(c); + + if (c instanceof Button) { + ((Button) c).addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + } + }); + } + } catch (Exception e) { + System.err.println("Could not instatiate " + cls.getName()); + } + } + addComponent(layout); + } + + private static final String DYNAMIC_IMAGE_NAME = "requestImage.png"; + + @Override + public boolean handleConnectorRequest(VaadinRequest request, + VaadinResponse response, String path) throws IOException { + if (DYNAMIC_IMAGE_NAME.equals(path)) { + // Create an image, draw the "text" parameter to it and output it to + // the browser. + String text = request.getParameter("text"); + if (text == null) { + text = DYNAMIC_IMAGE_NAME; + } + BufferedImage bi = getImage(text); + response.setContentType("image/png"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + + path + "\""); + ImageIO.write(bi, "png", response.getOutputStream()); + + return true; + } else { + return super.handleConnectorRequest(request, response, path); + } + } + + private BufferedImage getImage(String text) { + BufferedImage bi = new BufferedImage(150, 30, + BufferedImage.TYPE_3BYTE_BGR); + bi.getGraphics() + .drawChars(text.toCharArray(), 0, text.length(), 10, 20); + return bi; + } + + private BufferedImage getImage2(String text) { + BufferedImage bi = new BufferedImage(200, 200, + BufferedImage.TYPE_INT_RGB); + bi.getGraphics() + .drawChars(text.toCharArray(), 0, text.length(), 10, 20); + return bi; + } + + @Override + protected String getTestDescription() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected Integer getTicketNumber() { + // TODO Auto-generated method stub + return null; + } + +} |