WARNING - OUTDATED CONTENT!
This article is targeted on JSF 2.0/2.1. Since JSF 2.2 there's finally native file upload component in flavor of
<h:inputFile>
whose value can be tied to ajavax.servlet.http.Part
property. It's recommended to make use of it directly. See also this question & answer.
Introduction
The new Servlet 3.0 specification made uploading files really easy. However, because JSF 2.0 isn't initially designed to be primarily used on top of Servlet 3.0 and should be backwards compatible with Servlet 2.5, it lacks a standard file upload component. Until now you could have used among others Tomahawk's t:inputFileUpload for that. But as of now (December 2009) Tomahawk appears not to be "JSF 2.0 ready" yet and has problems here and there when being used on a JSF 2.0 environment. When you're targeting a Servlet 3.0 compatible container such as Glassfish v3, then you could also just create a custom JSF file upload component yourself.
To prepare, you need to have a Filter
which puts the parts of a multipart/form-data
request into the request parameter map before the FacesServlet kicks in. The FacesServlet namely doesn't have builtin facilities for this relies on the availablilty of the submitted input component values in the request parameter map. You can find it all here. Put the three classes MultipartMap, MultipartFilter and MultipartRequest in the classpath. The renderer of the custom file upload component relies on them in case of multipart/form-data requests.
Back to top
Custom component and renderer
With the new JSF 2.0 annotations it's now more easy to create custom components yourself. You don't need to hassle with somewhat opaque XML configurations anymore. I however only had a little hard time in figuring the best way to create custom components with help of annotations, because it's nowhere explained in the Java EE 6 tutorial nor the JSR314 - JSF 2.0 Specification. I am sure that the Sun JSF guys are also reading here, so here it is: Please work on that, it was already opaque in JSF 1.x and it should not be that more opaque in JSF 2.0!
At any way, I finally figured it with little help of Jim Driscoll's blog and exploring the JSF 2.0 source code.
First, let's look what we need: in the line of h:inputText component which renders a HTML input type="text" element, we would like to have a fictive h:inputFile component which renders a HTML input type="file" element. As h:inputText component is represented by a HtmlInputText class, we would thus like to have a HtmlInputFile class which extends HtmlInputText and overrides the renderer type so that it generates a HTML input type="file" element instead.
Okay, that's no big deal, so here it is:
/* * net/balusc/jsf/component/html/HtmlInputFile.java * * Copyright (C) 2009 BalusC * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library. * If not, see <http://www.gnu.org/licenses/>. */ package net.balusc.jsf.component.html; import javax.faces.component.FacesComponent; import javax.faces.component.html.HtmlInputText; /** * Faces component for <code>input type="file"</code> field. * * @author BalusC * @link http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html */ @FacesComponent(value = "HtmlInputFile") public class HtmlInputFile extends HtmlInputText { // Getters ------------------------------------------------------------------------------------ @Override public String getRendererType() { return "javax.faces.File"; } }
The nice thing is that this component inherits all of the standard attributes of HtmlInputText so that you don't need to redefine them (fortunately not; it would have been a fairly tedious task and a lot of code).
The value of the @FacesComponent annotation represents the component-type which is to be definied in the taglib xml file (shown later). The getRendererType() should return the renderer-type of the renderer class which is to be annotated using @FacesRenderer.
Extending the renderer is however quite a work when you want to be implementation independent, you need to take all possible attributes into account here as well. In this case we assume that you're going to use and stick to Mojarra 2.x forever (and thus not replace by another JSF implementation such as MyFaces sooner or later). Analogous with extending HtmlInputText to HtmlInputFile we thus want to extend its Mojarra-specific renderer TextRenderer to FileRenderer so that it renders a HTML input type="file" element instead.
/* * net/balusc/jsf/renderer/html/FileRenderer.java * * Copyright (C) 2009 BalusC * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library. * If not, see <http://www.gnu.org/licenses/>. */ package net.balusc.jsf.renderer.html; import java.io.File; import java.io.IOException; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.convert.ConverterException; import javax.faces.render.FacesRenderer; import net.balusc.http.multipart.MultipartRequest; import com.sun.faces.renderkit.Attribute; import com.sun.faces.renderkit.AttributeManager; import com.sun.faces.renderkit.RenderKitUtils; import com.sun.faces.renderkit.html_basic.TextRenderer; /** * Faces renderer for <code>input type="file"</code> field. * * @author BalusC * @link http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html */ @FacesRenderer(componentFamily = "javax.faces.Input", rendererType = "javax.faces.File") public class FileRenderer extends TextRenderer { // Constants ---------------------------------------------------------------------------------- private static final String EMPTY_STRING = ""; private static final Attribute[] INPUT_ATTRIBUTES = AttributeManager.getAttributes(AttributeManager.Key.INPUTTEXT); // Actions ------------------------------------------------------------------------------------ @Override protected void getEndTextToRender (FacesContext context, UIComponent component, String currentValue) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.startElement("input", component); writeIdAttributeIfNecessary(context, writer, component); writer.writeAttribute("type", "file", null); writer.writeAttribute("name", (component.getClientId(context)), "clientId"); // Render styleClass, if any. String styleClass = (String) component.getAttributes().get("styleClass"); if (styleClass != null) { writer.writeAttribute("class", styleClass, "styleClass"); } // Render standard HTMLattributes expect of styleClass. RenderKitUtils.renderPassThruAttributes( context, writer, component, INPUT_ATTRIBUTES, getNonOnChangeBehaviors(component)); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); RenderKitUtils.renderOnchange(context, component, false); writer.endElement("input"); } @Override public void decode(FacesContext context, UIComponent component) { rendererParamsNotNull(context, component); if (!shouldDecode(component)) { return; } String clientId = decodeBehaviors(context, component); if (clientId == null) { clientId = component.getClientId(context); } File file = ((MultipartRequest) context.getExternalContext().getRequest()).getFile(clientId); // If no file is specified, set empty String to trigger validators. ((UIInput) component).setSubmittedValue((file != null) ? file : EMPTY_STRING); } @Override public Object getConvertedValue(FacesContext context, UIComponent component, Object submittedValue) throws ConverterException { return (submittedValue != EMPTY_STRING) ? submittedValue : null; } }
Note that the @FacesRenderer annotation also specifies a component family of "javax.faces.Input" and that this is nowhere specified in our HtmlInputFile. That's also not needed, it's already inherited from HtmlInputText.
Now, to use the custom JSF 2.0 file upload component in Facelets we really need to define another XML file. It's however not a big deal. You fortunately don't need to define all the tag attributes as you should have done in case of JSP. Just define the namespace (which you need to specify in the xmlns attribute of the <html> tag), the tag name (to identify the tag in XHTML) and the component type (as definied in the @FacesComponent of the associated component class).
Create a new XML file at /WEB-INF/balusc.taglib.xml and fill it as follows:
<?xml version="1.0" encoding="UTF-8"?> <facelet-taglib xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd" version="2.0"> <namespace>http://balusc.net/jsf/html</namespace> <tag> <tag-name>inputFile</tag-name> <component> <component-type>HtmlInputFile</component-type> </component> </tag> </facelet-taglib>
You need to familarize Facelets with the new taglib in web.xml as follows:
<context-param> <param-name>javax.faces.FACELETS_LIBRARIES</param-name> <param-value>/WEB-INF/balusc.taglib.xml</param-value> </context-param>
Note, if you have multiple Facelets taglibs, then you can separate the paths with a semicolon ;.
Back to top
Basic use example
Here is a basic use example of a JSF managed bean and a Facelets page which demonstrates the working of all of the stuff. First the managed bean UploadBean:
package net.balusc.example.upload; import java.io.File; import java.util.Arrays; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; @ManagedBean @RequestScoped public class UploadBean { private String text; private File file; private String[] check; public void submit() { // Now do your thing with the obtained input. System.out.println("Text: " + text); System.out.println("File: " + file); System.out.println("Check: " + Arrays.toString(check)); } public String getText() { return text; } public File getFile() { return file; } public String[] getCheck() { return check; } public void setText(String text) { this.text = text; } public void setFile(File file) { this.file = file; } public void setCheck(String[] check) { this.check = check; } }
And now the Facelets page upload.xhtml:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:hh="http://balusc.net/jsf/html"> <h:head> <title>JSF 2.0 and Servlet 3.0 file upload test</title> <style>label { float: left; display: block; width: 75px; }</style> </h:head> <h:body> <h:form id="form" method="post" enctype="multipart/form-data"> <h:outputLabel for="text">Text:</h:outputLabel> <h:inputText id="text" value="#{uploadBean.text}" /> <br /> <h:outputLabel for="file">File:</h:outputLabel> <hh:inputFile id="file" value="#{uploadBean.file}" /> <h:outputText value="File #{uploadBean.file.name} successfully uploaded!" rendered="#{not empty uploadBean.file}" /> <br /> <h:selectManyCheckbox id="check" layout="pageDirection" value="#{uploadBean.check}"> <f:selectItem itemLabel="Check 1:" itemValue="check1" /> <f:selectItem itemLabel="Check 2:" itemValue="check2" /> </h:selectManyCheckbox> <h:commandButton value="submit" action="#{uploadBean.submit}" /> <h:messages /> </h:form> </h:body> </html>
Copy'n'paste the stuff and run it at http://localhost:8080/playground/upload.jsf (assuming that your local development server runs at port 8080 and that the context root of your playground web application project is called 'playground' and that you have the FacesServlet in web.xml mapped on *.jsf) and see it working! And no, you don't need to do anything with faces-config.xml, the managed bean is automagically found and initialized with help of the new JSF 2.0 annotations.
Note: this all is developed and tested with Eclipse 3.5 and Glassfish v3.
Back to top
Validate uploaded file
The lack of the support of @MultipartConfig annotation in the filter and JSF also implies that the size of the uploaded file can't be restricted by the maxFileSize annotation field. It is however possible to attach a simple validator to the custom component. Here's an example:
package net.balusc.example.upload; import java.io.File; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.FacesValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; @FacesValidator(value = "fileValidator") public class FileValidator implements Validator { private static final long MAX_FILE_SIZE = 10485760L; // 10MB. @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { File file = (File) value; if (file != null && file.length() > MAX_FILE_SIZE) { file.delete(); // Free resources! throw new ValidatorException(new FacesMessage(String.format( "File exceeds maximum permitted size of %d bytes.", MAX_FILE_SIZE))); } } }
You can attach it as follows:
<hh:inputFile validator="fileValidator" />
You can also use a f:validator instead:
<hh:inputFile> <f:validator validatorId="fileValidator" /> </hh:inputFile>
That should be it. Also no faces-config stuff is needed here thanks to the annotations.
Back to top
Copyright - GNU Lesser General Public License
(C) December 2009, BalusC