antropoff antropoff - 6 months ago 61
Ajax Question

JSF file download exception handling (how to prevent view rerendering)

Good day, friends!

First of all, I'm sorry for my English. Here is my question:

I'm new to JSF (2.0), and I'm trying to use BalusC algorythm for file download from managed bean. Function works correctly and "Save As..." dialog appears in browser. But I don't know how to return file download error message (exception, DB error etc. in backing bean method) without view reload/redirect.

My hidden button on the view:

<h:form id="detailsDecisionMainForm">
<h:inputHidden id="docId" value="" />
<h:commandButton id="downloadAction" action="#{detailsDecisionGridBean.downloadForm()}" style="display: none;" />
</h:form>


My managed bean (which scope can I use? I've tried request and view scopes) method:

public String downloadForm() {
log.fine("downloadForm call");
PdfService pdfService = new PdfServiceImpl();
ArmCommonService armCommonService = new ArmCommonServiceImpl();
String errorMessage = null;
try {
Long opUni = new Long(FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("detailsDecisionMainForm:docId"));
log.fine("Document opUni = " + opUni);
DocXML docXML = armCommonService.getDocXMLById(opUni);
if (docXML != null) {
File pdfFile = pdfService.getPdfReport(docXML.getXml(), docXML.getType());
if (pdfFile != null) {
DownloadUtils.exportPdf(pdfFile);
} else {
log.log(Level.SEVERE, "downloadForm error: pdf generation error");
errorMessage = "PDF-document generation error.";
}
} else {
log.log(Level.SEVERE, "downloadForm error: xml not found");
errorMessage = "XML-document not found.";
}
} catch (Exception ex) {
log.log(Level.SEVERE, "downloadForm exception: " + ex.getMessage());
errorMessage = "File download exception.";
}
if (errorMessage != null) {
FacesContext.getCurrentInstance().addMessage("detailsDecisionMainForm:downloadAction", new FacesMessage(errorMessage));
}
return null;
}


DownloadUtils.exportPdf() procedure works correctly:

public static void exportPdf(File file) throws IOException {
InputStream fileIS = null;
try {
log.fine("exportPdf call");
fileIS = new FileInputStream(file);
FacesContext fc = FacesContext.getCurrentInstance();
ExternalContext ec = fc.getExternalContext();
ec.responseReset();
ec.setResponseContentType(APPLICATION_PDF_UTF_8);
byte[] buffer = ByteStreams.toByteArray(fileIS);
ec.setResponseContentLength(buffer.length);
ec.setResponseHeader(HttpHeaders.CONTENT_DISPOSITION, String.format(CONTENT_DISPOSITION_VALUE, new String(file.getName().getBytes(StandardCharsets.UTF_8))));
ec.getResponseOutputStream().write(buffer);
fc.responseComplete();
} catch (Exception ex) {
log.log(Level.SEVERE, "exportPdf exception: " + ex.getMessage());
} finally {
if (fileIS != null) {
fileIS.close();
log.fine("exportPdf inputstream file closed");
}
}
}


What can I do to prevent view rerendering after downloadForm() error/exception? And how can I show javascript alert() with message text (in future - jQuery.messager panel with error text)?

Thank you!

Answer

In order to prevent a full page reload, you have to submit form by ajax. But, in order to be able to download a file, you have to turn off ajax. This doesn't go well together.

Your best bet is to split the action in two requests. First send an ajax request which creates the file on a temporary location in the server side. When this fails, you can just display a faces message the usual way. When this succeeds, you can just automatically trigger the second request by submitting a hidden non-ajax command button via conditionally rendered JavaScript. This second request can then just stream the already successfully created file from the temporary location to the response.

A similar question was already asked and answered before: Conditionally provide either file download or show export validation error message. But this involves PrimeFaces and OmniFaces. Below is the standard JSF approach:

<h:form id="form">
    ...
    <h:commandButton value="Export" action="#{bean.export}">
        <f:ajax ... render="result" />
    </h:commandButton>
    <h:panelGroup id="result">
        <h:messages />
        <h:commandButton id="download" action="#{bean.download}"
            style="display:none" />
        <h:outputScript rendered="#{not facesContext.validationFailed}">
            document.getElementById("form:button").onclick();
        </h:outputScript>
    </h:panelGroup>
</h:form>

And use this @ViewScoped bean (logic is based on your existing logic). Basically, just get hold of the File as instance variable during export action (ajax) and then stream it during download action (non-ajax).

private File pdfFile;

public void export() {
    try {
        pdfFile = pdfService.getPdfReport(...);
    } catch (Exception e) {
        context.addMessage(...);
    }
}

public void download() throws IOException {
    DownloadUtils.exportPdf(pdfFile);
}

See also: