javabinaryjsf-2cdiserving

Serving files with JSF 2 / CDI, using bookmarkable URLs


My main question is : Is there a "good practice" to serve binary files (PDF, docs, etc) using JSF 2 with CDI, and using bookmarkable URLs ?

I've read the JSF 2 spec (JSR 314) and I see it exists a "Resource Handling" paragraph. But it seems to be used only to serve static files put in the war or jar files. I didn't really understood if it exists a way to interact here by registering some specific ResourceHandler ...

Actually, I was used to Seam's 2 way to do that : extending the AbstractResource class with getResource(HttpServletRequest, HttpServletResponse) method and getResourcePath() to declare which path to serve after <webapp>/seam/resource/ URL prefix and declaring the SeamResourceServlet in the web.xml file.

Here is what I did.

I've first saw How to download a file stored in a database with JSF 2.0 and tried to implement it.

<f:view ...

    <f:metadata>
        <f:viewParam name="key" value="#{containerAction.key}"/>
        <f:event listener="#{containerAction.preRenderView}" type="preRenderComponent" />
    </f:metadata>

    ...

    <rich:dataGrid columns="1" value="#{containerAction.container.files}" var="file">
        <rich:panel>
                <h:panelGrid columns="2">
                    <h:outputText value="File Name:" />
                    <h:outputText value="#{file.name}" />
                </h:panelGrid>
                <h:form>
                    <h:commandButton value="Download" action="#{containerAction.download(file.key)}" />
                </h:form>
        </rich:panel>
    </rich:dataGrid>

And here is the beans :

@Named
@SessionScoped
public class ContainerAction {

    private Container container;

    /// Injections
    @Inject @DefaultServiceInstance
    private Instance<ContainerService> containerService;

    /// Control methods
    public void preRenderView(final ComponentSystemEvent event) {
        container = containerService.get().loadFromKey(key);
    }

    /// Action methods
    public void download(final String key) throws IOException {
        final FacesContext facesContext = FacesContext.getCurrentInstance();

        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();

        final ContainerFile containerFile = containerService.get().loadFromKey(key);
        final InputStream containerFileStream = containerService.get().read(containerFile);

        response.setHeader("Content-Disposition", "attachment;filename=\""+containerFile.getName()+"\"");
        response.setContentType(containerFile.getContentType());
        response.setContentLength((int) containerFile.getSize());

        IOUtils.copy(containerFileStream, response.getOutputStream());

        response.flushBuffer();

        facesContext.responseComplete();
    }

    /// Getters / setters
    public Container getContainer() {
        return container;
    }
}

Here I had to switch to Tomcat 7 (I was using 6) in order to interpret correctly that EL expression. With @SessionScoped it worked, but not with @RequestScoped (when I clicked the button, nothing happend).

But then I wanted to use a link instead of a button.

I tried <h:commandLink value="Download" action="#{containerAction.download(file.key)}" /> but it generates some ugly javascript link (not bookmarkable).

Reading the JSF 2 spec, it seems that there is a "Bookmarkability" feature, but it is not realy clear how to use it.

Actually, it seems to work only with views, so I tried to create an empty view and created a h:link :

<h:link outcome="download.xhtml" value="Download">
    <f:param name="key" value="#{file.key}"/>
</h:link>
<f:view ...>
    <f:metadata>
        <f:viewParam name="key" value="#{containerFileDownloadAction.key}"/>
        <f:event listener="#{containerFileDownloadAction.download}" type="preRenderComponent" />
    </f:metadata>
</f:view>
@Named
@RequestScoped
public class ContainerFileDownloadAction {

    private String key;

    @Inject @DefaultServiceInstance
    private Instance<ContainerService> containerService;

    public void download() throws IOException {
        final FacesContext facesContext = FacesContext.getCurrentInstance();

        // same code as previously
        ...

        facesContext.responseComplete();
    }


    /// getter / setter for key
    ...
}

But then, I had a java.lang.IllegalStateException: "getWriter()" has already been called for this response.

Logic as when a view initiates, it uses getWritter to initialize the response.

So I created a Servlet which does the work and created the following h:outputLink :

<h:outputLink value="#{facesContext.externalContext.request.contextPath}/download/">
    <h:outputText value="Download"/>
    <f:param name="key" value="#{file.key}"/>
</h:outputLink>

But even if that last technique gives me a bookmarkable URL for my file, it is not really "JFS 2" ...

Do you have some advice ?


Solution

  • There is in fact a direct solution to this problem using PrettyFaces URLRewriteFilter -> http://ocpsoft.org/prettyfaces/serving-dynamic-file-content-with-prettyfaces/

    This blog explains how to do exactly what you want to do, without having to use an entirely new MVC framework.