Minggu, 02 Februari 2014

Uploading files with JSF 2.0 and Servlet 3.0



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 whose value can be tied to a javax.servlet.http.Part property. It's recommended to make use of it directly.







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 .
*/


package net.balusc.jsf.component.html;

import javax.faces.component.FacesComponent;
import javax.faces.component.html.HtmlInputText;

/**
* Faces component for input type="file" 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 .
*/


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 input type="file" 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 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:



 version="1.0" encoding="UTF-8"?>

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">

http://balusc.net/jsf/html

inputFile

HtmlInputFile




You need to familarize Facelets with the new taglib in web.xml as follows:






javax.faces.FACELETS_LIBRARIES
/WEB-INF/balusc.taglib.xml




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:





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">

</span>JSF 2.0 and Servlet 3.0 file upload test<span class="codetag">



id="form" method="post" enctype="multipart/form-data">
for="text">Text:
id="text" value="#{uploadBean.text}" />


for="file">File:
id="file" value="#{uploadBean.file}" />
value="File #{uploadBean.file.name} successfully uploaded!"
rendered="#{not empty uploadBean.file}" />


id="check" layout="pageDirection" value="#{uploadBean.check}">
itemLabel="Check 1:" itemValue="check1" />
itemLabel="Check 2:" itemValue="check2" />

value="submit" action="#{uploadBean.submit}" />





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:





validator="fileValidator" />



You can also use a f:validator instead:






validatorId="fileValidator" />




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




Source:http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html

Tidak ada komentar:

Posting Komentar