开发者

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 v开发者_运维百科oid 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 ?


I agree with BalusC. Typically an application is not purely a JSF application but a Java EE application.

It's not for nothing that other things than JSF views exist for handling http requests in Java EE. In Java EE 6, your named CDI bean can btw also directly be mapped to a path using JAX-RS. This is an alternative to using Servlets. In that case you would use @Produces and @Path (see e.g. Input and Output binary streams using JERSEY?).

On the other hand, one advantage of <f:viewParam> in JSF is that you can easily attach validators to it. Neither Servlets nor JAX-RS resources have support for that at the moment.

<h:link> is also more comfortable to use than writing <h:outputLink value="#{facesContext.externalContext.request.contextPath}/..."> all the time. This can however be mitigated by wrapping this part in a Facelets tag or composite component.

(It would be great I think if a future version of the spec provided a link tag in JSF to link directly to JAX-RS resources (with optional container startup validation to ensure the link is legal)).


JSF is from the beginning on designed to be a MVC framework, not to be some REST file service.

A servlet is perfectly fine for the job. Annotate it with @WebServlet to get a better Java EE 6 feeling.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜