]> source.dussan.org Git - vaadin-framework.git/commitdiff
FileDownloader for starting downloads with any component (#9524) 48/148/2
authorLeif Åstrand <leif@vaadin.com>
Tue, 23 Oct 2012 12:59:54 +0000 (15:59 +0300)
committerLeif Åstrand <leif@vaadin.com>
Tue, 23 Oct 2012 12:59:54 +0000 (15:59 +0300)
* Based on patch by Pekka Hyvönen

Change-Id: I9263078ffc624f9cabec6c25264920dfdb430efe

client/src/com/vaadin/client/extensions/FileDownloaderConnector.java [new file with mode: 0644]
server/src/com/vaadin/server/FileDownloader.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/FileDownloaderTest.java [new file with mode: 0644]

diff --git a/client/src/com/vaadin/client/extensions/FileDownloaderConnector.java b/client/src/com/vaadin/client/extensions/FileDownloaderConnector.java
new file mode 100644 (file)
index 0000000..d76efcc
--- /dev/null
@@ -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 (file)
index 0000000..a07c0d3
--- /dev/null
@@ -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 (file)
index 0000000..d5f447c
--- /dev/null
@@ -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;
+    }
+
+}