Sunday, May 21, 2006

PDF handling

Read and open the PDF in a new browser window

The goal is to use JSF to open a PDF file inline in a new browser window without showing the full PDF path and filename for some (security) reasons. You can use either a h:commandLink or a h:commandButton for this:

<h:form>
    <h:commandLink value="Download PDF" action="#{myBean.downloadPDF}" target="_blank" />
</h:form>
<h:form target="_blank">
    <h:commandButton value="Download PDF" action="#{myBean.downloadPDF}" />
</h:form>

Note: when using an ajax-capable command button/link from a library such as PrimeFaces, then don't forget to turn off the ajax capability by e.g. adding ajax="false" in case of PrimeFaces command components. It's namely not possible to download files using ajax, for the simple reason that JavaScript is due to security restrictions not capable of forcing a Save As dialogue from the client side on.

The downloadPDF() method of the backing bean MyBean.java sends the PDF to the HttpServletResponse as follows:

package mypackage;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletResponse;

public class MyBean {

    // Constants ----------------------------------------------------------------------------------

    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.

    // Actions ------------------------------------------------------------------------------------

    public void downloadPDF() throws IOException {

        // Prepare.
        FacesContext facesContext = FacesContext.getCurrentInstance();
        ExternalContext externalContext = facesContext.getExternalContext();
        HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();

        File file = new File(getFilePath(), getFileName());
        BufferedInputStream input = null;
        BufferedOutputStream output = null;

        try {
            // Open file.
            input = new BufferedInputStream(new FileInputStream(file), DEFAULT_BUFFER_SIZE);

            // Init servlet response.
            response.reset();
            response.setHeader("Content-Type", "application/pdf");
            response.setHeader("Content-Length", String.valueOf(file.length()));
            response.setHeader("Content-Disposition", "inline; filename=\"" + getFileName() + "\"");
            output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

            // Write file contents to response.
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }

            // Finalize task.
            output.flush();
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }

        // Inform JSF that it doesn't need to handle response.
        // This is very important, otherwise you will get the following exception in the logs:
        // java.lang.IllegalStateException: Cannot forward after response has been committed.
        facesContext.responseComplete();
    }

    // Helpers (can be refactored to public utility class) ----------------------------------------

    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException e) {
                // Do your thing with the exception. Print it, log it or mail it. It may be useful to 
                // know that this will generally only be thrown when the client aborted the download.
                e.printStackTrace();
            }
        }
    }

}

The names of the getFilePath() and getFileName() are self-explaining, so there is no need to describe those methods.

If you don't care about showing request parameters or filenames in the URL, then you can also consider to use a FileServlet.

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) May 2006, BalusC

Restore tabbedpanel

Maintain the tab selection after form submission

The IBM JSF version as is in Websphere Studio Application Developer 5.x doesn't return to the tab selected after submitting a form in a certain tab of the odc:tabbedPanel, while the IBM JSF version as is in WSAD 6.x does. This practice describes the javascript hack for IBM JSF in WSAD 5.x to maintain the tab selected after a form submission.

Use Javascript

The trick is to put boolean getter methods in the backing bean and handle the tab selection using Javascript. The boolean getter methods should return true if the appropriate form has been submitted before. All ODC elements are stored in the Javascript ODCRegistry object, you can retrieve the ODC element as object using getClientControl(). With the restoreUIState() function you can set the tab selection state. It is important that the managed bean has a session scope instead of a request scope. Otherwise, the data of the managed bean (vars, state, etc) will be garbaged directly after one request.

The relevant XML code of the faces-config file:

<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

The basic JSF code example for the JSP file:

<body>
    <hx:scriptCollector id="scriptCollector1">
        ...
        <odc:tabbedPanel id="tabbedPanel1">
            <odc:bfPanel id="bfpanelForm1">
                <h:form id="form1">
                    ...
                    <h:commandButton value="submit" action="#{myBean.submitForm1}" />
                </h:form>
            </odc:bfPanel>
            <odc:bfPanel id="bfpanelForm2">
                <h:form id="form2">
                    ...
                    <h:commandButton value="submit" action="#{myBean.submitForm2}" />
                </h:form>
            </odc:bfPanel>
            ...
        </odc:tabbedPanel>
        ...
    </hx:scriptCollector>
    <f:verbatim>
        <script>
            panel = ODCRegistry.getClientControl('tabbedPanel1');
            panel.restoreUIState('bfpanelForm' + 
                </f:verbatim><h:outputText value="#{myBean.submittedForm}"/><f:verbatim>);
        </script>
    </f:verbatim>
</body>

Note: in JSF 1.2 those f:verbatim tags aren't needed however.

It is important that the Javascript is executed after the hx:scriptCollector part, otherwise the panel state will be overriden by the default value.

The relevant java code of the backing bean MyBean.java should look like:

package mypackage;

public class MyBean {

    // Init --------------------------------------------------------------------------------------

    private int submittedForm;

    // Actions -----------------------------------------------------------------------------------

    public void submitForm1() {
        // Do your action thing for form1.
        ...
        submittedForm = 1;
    }

    public void submitForm2() {
        // Do your action thing for form2.
        ...
        submittedForm = 2;
    }

    // Getters -----------------------------------------------------------------------------------

    public int getSubmittedForm() {
        return submittedForm;
    }

}

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) May 2006, BalusC