import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
@SuppressWarnings("serial")
public class DownloadStream implements Serializable {
+ public static final String CONTENT_DISPOSITION = "Content-Disposition";
+
/**
* Maximum cache time.
*/
}
}
- // suggest local filename from DownloadStream if
- // Content-Disposition
- // not explicitly set
- String contentDispositionValue = getParameter("Content-Disposition");
- if (contentDispositionValue == null) {
- contentDispositionValue = "filename=\"" + getFileName()
- + "\"";
- response.setHeader("Content-Disposition",
- contentDispositionValue);
+ // Content-Disposition: attachment generally forces download
+ String contentDisposition = getParameter(CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ contentDisposition = getContentDispositionFilename(getFileName());
}
+ response.setHeader(CONTENT_DISPOSITION, contentDisposition);
+
int bufferSize = getBufferSize();
if (bufferSize <= 0 || bufferSize > Constants.MAX_BUFFER_SIZE) {
bufferSize = Constants.DEFAULT_BUFFER_SIZE;
}
}
+ /**
+ * Returns the filename formatted for inclusion in a Content-Disposition
+ * header. Includes both a plain version of the name and a UTF-8 version
+ *
+ * @since
+ * @param filename
+ * The filename to include
+ * @return A value for inclusion in a Content-Disposition header
+ */
+ public static String getContentDispositionFilename(String filename) {
+ try {
+ String encodedFilename = URLEncoder.encode(filename, "UTF-8");
+ return String.format("filename=\"%s\"; filename*=utf-8''%s",
+ encodedFilename, encodedFilename);
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+
+ }
+
+ public static Logger getLogger() {
+ return Logger.getLogger(DownloadStream.class.getName());
+ }
+
/**
* Helper method that tries to close an output stream and ignores any
* exceptions.
}
stream = ((ConnectorResource) resource).getStream();
- if (stream.getParameter("Content-Disposition") == null) {
- // Content-Disposition: attachment generally forces download
- stream.setParameter("Content-Disposition",
- "attachment; filename=\"" + stream.getFileName() + "\"");
+ String contentDisposition = stream
+ .getParameter(DownloadStream.CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ contentDisposition = "attachment; "
+ + DownloadStream.getContentDispositionFilename(stream
+ .getFileName());
}
+ stream.setParameter(DownloadStream.CONTENT_DISPOSITION,
+ contentDisposition);
+
// Content-Type to block eager browser plug-ins from hijacking
// the file
if (isOverrideContentType()) {
stream.writeResponse(request, response);
return true;
}
+
}
--- /dev/null
+package com.vaadin.server;
+
+import static org.mockito.Matchers.contains;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class DownloadStreamTest {
+ private String filename = "日本語.png";
+ private DownloadStream stream;
+
+ @Before
+ public void setup() {
+ stream = new DownloadStream(mock(InputStream.class), "", filename);
+ }
+
+ @Test
+ public void contentDispositionFilenameIsUtf8Encoded() throws IOException {
+ VaadinResponse response = mock(VaadinResponse.class);
+
+ stream.writeResponse(mock(VaadinRequest.class), response);
+
+ String encodedFileName = URLEncoder.encode(filename, "utf-8");
+ verify(response).setHeader(eq(DownloadStream.CONTENT_DISPOSITION),
+ contains(String.format("filename=\"%s\";", encodedFileName)));
+ verify(response)
+ .setHeader(
+ eq(DownloadStream.CONTENT_DISPOSITION),
+ contains(String.format("filename*=utf-8''%s",
+ encodedFileName)));
+ }
+}
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinResponse;
import com.vaadin.tests.components.embedded.EmbeddedPdf;
-import com.vaadin.tests.util.Log;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Layout;
import com.vaadin.ui.NativeButton;
-public class FileDownloaderTest extends AbstractTestUI {
+public class FileDownloaderTest extends AbstractTestUIWithLog {
private AbstractComponent firstDownloadComponent;
- private Log log = new Log(5);
@Override
protected void setup(VaadinRequest request) {
- addComponent(log);
-
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() {
} 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);
+ Button downloadUtf8File = new Button("Download UTF-8 named file");
+ FileDownloader fd = new FileDownloader(new ClassResource(
+ new EmbeddedPdf().getClass(), "åäö-日本語.pdf"));
+ fd.setOverrideContentType(false);
+ fd.extend(downloadUtf8File);
+ addComponent(downloadUtf8File);
+
addComponent(new Button("Remove first download button",
new ClickListener() {
FileDownloader e = (FileDownloader) firstDownloadComponent
.getExtensions().iterator().next();
e.remove();
- log.log("FileDownload detached");
+ log("FileDownload detached");
}
}));
}
return bi;
}
- @Override
- protected String getTestDescription() {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- protected Integer getTicketNumber() {
- // TODO Auto-generated method stub
- return null;
- }
-
}