Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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>
like image 441
cheffe Avatar asked Sep 05 '14 13:09

cheffe


1 Answers

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:

  • File download from JSF with a rendered response
like image 169
Xtreme Biker Avatar answered Oct 22 '22 00:10

Xtreme Biker