--- /dev/null
+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();
+ }
+ }
+
+}
--- /dev/null
+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;
+ }
+ }
+}
--- /dev/null
+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;
+ }
+
+}