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 ?
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.