How to stream a file download and display a JSF faces message?

18,524

Since you're actually performing a file download response and not a JSF one, it's not possible for your message to be added while the same request happens. The most clean solution for me, avoiding hacky asynchronous requests is to use a @ViewScoped bean and do your task in two steps. So, to have a button for preparing your file, notifying the user later on and allowing him to download it when it's ready:

@ManagedBean
@ViewScoped
public class ExportController implements Serializable {

    private byte[] exportContent;

    public boolean isReady() {
        return exportContent != null;
    }

    public void export() {
        FacesContext fc = FacesContext.getCurrentInstance();
        ExternalContext ec = fc.getExternalContext();
        ec.responseReset();
        ec.setResponseContentType("text/plain");
        ec.setResponseContentLength(exportContent.length);
        String attachmentName = "attachment; filename=\"export.txt\"";
        ec.setResponseHeader("Content-Disposition", attachmentName);
        try {
            OutputStream output = ec.getResponseOutputStream();
            Streams.copy(new ByteArrayInputStream(exportContent), output, false);
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        fc.responseComplete();
    }

    public void prepareFile() {
        exportContent = "Hy Buddys, thanks for the help!".getBytes();
        // here something bad happens that the user should know about
        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage("record 2 was flawed"));
    }
}
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui">

<f:view contentType="text/html">
    <h:body>
        <h:form>
            <h:messages id="messages" />
            <h:commandButton value="Prepare"
                action="#{exportController.prepareFile}" />
            <h:commandButton id="download" value="Download"
                disabled="#{not exportController.ready}"
                action="#{exportController.export()}" />
        </h:form>
    </h:body>
</f:view>
</html>

Note this solution could be valid for small files (their entire content is stored in memory while user keeps in the same view). However, if you're going to use it with large files (or large number of users) your best is to store its content in a temporary file and display a link to it instead of a download button. That's what @BalusC suggests in the reference below.

See also:

Share:
18,524
cheffe
Author by

cheffe

Updated on June 04, 2022

Comments

  • cheffe
    cheffe almost 2 years

    We are streaming a binary file to our users, following the procedure elaborated in the SO question How to provide a file download from a JSF backing bean?

    In general the workflow works as intended, but during the generation of the export file recoverable errors may occur and we want to display these as a warning to the user. The file itself shall still be generated in that case. So we want that export to continue and display faces messages.

    Just to put emphasis on this: Yes, there is something not OK with the data, but our users want the export to continue and receive that flawed file anyway. Then they want to have a look at the file, contact their vendor and send him a message about the flaw.

    So I need the export to finish in any case.

    But it does not work out as we want it to. I have created a simplified example to illustrate our approach.

    As alternative we are considering a Bean that will be hold the messages and display them after the export. But probably there is a way with JSF built-in mechanisms to achieve this.

    Controller

    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import javax.faces.application.FacesMessage;
    import javax.faces.bean.ManagedBean;
    import javax.faces.bean.RequestScoped;
    import javax.faces.context.ExternalContext;
    import javax.faces.context.FacesContext;
    import org.apache.tomcat.util.http.fileupload.util.Streams;
    
    @ManagedBean
    @RequestScoped
    public class ExportController {
    
        public void export() {
            FacesContext fc = FacesContext.getCurrentInstance();
            ExternalContext ec = fc.getExternalContext();
    
            byte[] exportContent = "Hy Buddys, thanks for the help!".getBytes();
            // here something bad happens that the user should know about
            // but this message does not go out to the user
            fc.addMessage(null, new FacesMessage("record 2 was flawed"));
    
            ec.responseReset();
            ec.setResponseContentType("text/plain");
            ec.setResponseContentLength(exportContent.length);
            String attachmentName = "attachment; filename=\"export.txt\"";
            ec.setResponseHeader("Content-Disposition", attachmentName);
            try {
                OutputStream output = ec.getResponseOutputStream();
                Streams.copy(new ByteArrayInputStream(exportContent), output, false);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
    
            fc.responseComplete();
        }
    }
    

    JSF Page

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:h="http://java.sun.com/jsf/html"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:ui="http://java.sun.com/jsf/facelets"
          xmlns:p="http://primefaces.org/ui">
    
        <f:view contentType="text/html">
            <h:body>
                <h:form prependId="false">
                    <h:messages id="messages" />
                    <h:commandButton id="download" value="Download"
                                     actionListener="#{exportController.export()}" />
                </h:form>
            </h:body>
        </f:view>
    </html>
    
  • BalusC
    BalusC over 9 years
    You could use JS to invoke the next action.
  • douglaslps
    douglaslps about 9 years
    @BalusC it would be very nice to see your JS solution. Can you post a link?
  • Ani Menon
    Ani Menon about 8 years
    Adding more description & useful content to support your answer would be helpful. Otherwise this will just get deleted.