Download generated file - better solution

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:

  1. you need to provide InputStream. Many times, it forces you into using ByteArrayOutputStream, which is unusable with large data.
  2. 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)
  3. 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.

We do something similar, but we just use pure links and let the servlet handle the entire generation and download response. We found download streams to be too unreliable (like we couldn’t even get PDFs to work nicely when PUSH was enabled, despite the fact we could generate the dynamic PDFs without issue).

Another issue is that the FileDownloader doesn’t work on menu items, action handlers for tree elements, or anything else that’s not an AbstractComponent.

What is nice about your soution is the dynamic download key is only generated after they click to download, whereas we have to generate one for each link we produce and so have to ensure we clear our keys as appropriate (including when the view is detached).

Hi Ovil,
this way
could help you, and doesn’t matter the size of your file

David, my solution is for dynamically generated content. I have a form, which defines the contents of the report. As the user clicks OK, I validate the form and eventually generate a file. My solution requires two roundtrips, your only one.

Jorge: this is practically the same as my download servlet, it’s just using vaadin specific handling instead of standard Servlet API.