We (and many other users) had problem if you want the user to download a generated file. The recommended way with FileDownloader has many disadvantages:
- you need to provide InputStream. Many times, it forces you into using ByteArrayOutputStream, which is unusable with large data.
- you cannot do any other work except to provide the file (e.g. when you extend an OK button under a form, you cannot validate it upon click)
- if you fail, the user will be provided with empty file. The error window you opened will be displayed the next time he clicks that button.
The old way with
Page.getCurrent().open(Resource, windowName)
is deprecated, I don’t know why. But it still has problem 1.
My solution is following. I store a DownloadHandler in the session under a key. A link with the key is provided to the user to download. After the browser starts the download, we can generate the file to OutputStream. This solution still partly has the problem 3: if the generation fails after the response is commited, the user won’t see the error and will receive corrupted file.
Servlet class:
package mypackage;
import java.io.*;
import java.util.UUID;
import java.util.concurrent.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import com.vaadin.server.VaadinService;
import com.vaadin.server.WrappedSession;
public class OutputStreamDownloader extends HttpServlet {
private static final String SESSION_KEY = OutputStreamDownloader.class.getName() + "tasks";
public static interface DownloadHandler extends Serializable {
public void handleDownload(HttpServletRequest req, HttpServletResponse resp) throws IOException;
}
@SuppressWarnings("unchecked")
public static String getDownloadKey(DownloadHandler downloadHandler) {
WrappedSession session = VaadinService.getCurrentRequest().getWrappedSession();
ConcurrentMap<string, downloadhandler=""> map;
synchronized (OutputStreamDownloader.class) {
map = (ConcurrentMap<string, downloadhandler="">) session.getAttribute(SESSION_KEY);
if (map == null) {
map = new ConcurrentHashMap<>();
session.setAttribute(SESSION_KEY, map);
}
}
String key = UUID.randomUUID().toString();
map.put(key, downloadHandler);
return key;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
@SuppressWarnings("unchecked")
ConcurrentMap<string, downloadhandler=""> map = (ConcurrentMap<string, downloadhandler="">)
req.getSession().getAttribute(SESSION_KEY);
String pathInfo = req.getPathInfo();
DownloadHandler handler;
if (map == null || pathInfo == null || ! pathInfo.startsWith("/") || (handler = map.remove(pathInfo.substring(1))) == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
try {
handler.handleDownload(req, resp);
}
catch (Exception e) {
log.error(null, e);
}
}
}
Map the servlet to URL in web.xml (or use annotations, if you prefer):
<servlet>
<servlet-name>OutputStreamDownloader</servlet-name>
<servlet-class>mypackage.OutputStreamDownloader</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>OutputStreamDownloader</servlet-name>
<url-pattern>/download/*</url-pattern>
</servlet-mapping>
And use it:
Page.getCurrent().open("download/" + OutputStreamDownloader.getDownloadKey((req, resp) -> {
resp.setContentType("application/zip");
resp.setHeader("Content-Disposition", "attachment; filename=largedata.zip");
// Set the length only if you know precise length in advance. If you set it, the user will see
// the download progress.
//resp.setContentLength(....);
OutputStream str = res.getOutputStream();
}), "_new");
The file is downloadable only once. The link is only valid to the active session.