Tuesday, January 10, 2006

Creating downloadable files

WARNING: This blog entry was imported from my old blog on blogs.sun.com (which used different blogging software), so formatting and links may not be correct.





My brother has been visiting me this last couple of weeks.
It's been really nice and fun - see the picture on the right for my
recycling bin a couple of days ago...



Last night we took a look at an application he has developed using
Creator 2 (EA2). It's a nice little web application for his company that
lets customers download patches based on privileges, installed
software, etc. It had some weird bugs. So I went looking in the
forums, and sure enough, there's a real Gotcha! with implementing
live file downloading. Several people had tried and the solution
was not posted. So, a blog entry might be in order!



There are many use cases for live file downloading. (By "live file"
I mean that you are not just providing a static hyperlink to a file
you are just deploying with your web application - you instead want
to provide the file to the browser when they click on something, but
the contents of the file might be generated on the fly, or at least
the actual file to be downloaded is determined at runtime).



For example, you may want to let users click to download and view a
PDF report based on current data in the web application, or perhaps
even use something like POI
to generate Excel-formatted spreadsheet files.



The first thing you have to do is write an action handler. This is
invoked when the user clicks the download link or button. Double click
on the component to get a skeleton, then write something like this:


public String button1_action() {
String filename = "foo.pdf"; // Filename suggested in browser Save As dialog
String contentType = "application/pdf"; // For dialog, try application/x-download
byte[] data = ; // File contents to be written. Sorry, YOU have to do this part!

FacesContext fc = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse)fc.getExternalContext().getResponse();
response.setHeader("Content-disposition", "attachment; filename=" + filename);
response.setContentLength(data.length);
response.setContentType(contentType);
ServletOutputStream out = response.getOutputStream();
out.write(data);
fc.responseComplete();

return null;
}



You may have been tempted to add the above code in the action handler for
a hyperlink (or link action). That seems really natural - most "download links" on the
web are just that - actual links. Indeed, that's what my brother
had done.



And that's the gotcha. If you do that, the above code will work, but
as soon as you've clicked the link to download, your web app starts to
act funny - if you click on any other hyperlinks, the download will
be initiated again! It's as if the above code "corrupts" the webapp.



The solution is really simple. Just use a download button instead
of a link!
If you hook the download code up to a button action handler, everything
will work as expected. And having a button rather than a link does make
some sense when what you're doing is asking for something to be
generated (a live file) rather than simply referencing a static
resource.



<speculation range="wild">

JSF jumps through some hoops to make HTML hyperlinks behave like
proper "actions" - via tricks like using an input hidden field
in the form etc. This might be what's causing the weird link
anomaly. I will check with the JSF guys to see if this has been addressed
in newer JSF releases.


</speculation>


3 comments:

  1. Hello,
    I am trying to navigate after an export data from JSF (I use POI to generate Excel-formatted spreadsheet files "CSV"). For that I use response.getOutPutStream() which works good (the export works),
    but after this export, JSF lost the navigation (when I click on "h:commandLink" or "h:commandButton" to do other Action ...). I think that my data and my " <t:saveState id="xxxManagerBean" value="xxxManagerBean"> " are lost .
    code for export:
    //I use a download button : "h:commandButton immediate="true" action="#{myManagerBean.exportAction}" "
    //FunctionAction.
    public String exportAction() {
    FacesContext context = FacesContext.getCurrentInstance();
    HttpServletResponse response =
    (HttpServletResponse)context.getExternalContext().getResponse();
    response.setContentType("application/x-zip");
    response.setContentType("Cache-Control", "no-store");
    response.setContentType("Pragma", "");
    response.setHeader("Content-disposition",
    "attachment; filename=file.zip");
    try {
    exportExcel = new CSVExport(response.getOutputStream(), "nameFile.csv");
    exportExcel.setCharSeparator(..);
    exportExcel.exportData("List" or "ResultSet");
    exportExcel.closeFile();
    response.getOutputStream().flush();
    response.getOutputStream().close();
    context.responseComplete();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    Perhaps, this problem is caused by "context.responseComplete();" or the saveState lost data when I work white response !!!.
    There is the same problem whith the "<h:commandLink target="myNewPage"/>", I lost my navigation.
    Thanks in Advance.

    ReplyDelete
  2. Thanks for reply,
    In my JSF code, I have a tomahawk tag [t:saveState id="xxxManagerBean" value="xxxManagerBean"]. The export data functions correctly, but the navigation is lost because my data are lost after the export.
    I think that the tomahawk tag [t:saveState ... ] does not keep good the values after export.
    The same problem occurs as you use the attribute Target in [h:commandLink target="myNewPage"/>"]. after export, I lost my navigation.
    Thanks in Advance.

    ReplyDelete
  3. Sorry, I don't know anything about Tomahawk. You cannot always mix and match web frameworks -- JSF relies on redirects for navigation so if tomahawk is messing with state it could cause problems. It might be possible to make this work but I don't know anything about it - sorry.

    ReplyDelete