Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

JSF 2.0で、ファイルアップロードのコンポーネントを作成し、SpringのMultipartFileオブジェクトとして扱う

参考にさせていただいたサイト

The BalusC Code: Uploading files with JSF 2.0 and Servlet 3.0
http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html


JSFにはファイルアップロードコンポーネントがないらしい。


CommonsのFileUploadで扱うわけだけど、
さらにSpringのMultipartFileにしてManagedBeanのプロパティにセットするようにした。


まずFilter。当然、web.xmlで定義してね。

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class FileUploadFilter implements Filter {
	private String tmpDirectoryPath;
	private String encoding;

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		this.tmpDirectoryPath = System.getProperty("java.io.tmpdir");
		String enc = filterConfig.getInitParameter("encoding");
		this.encoding = enc == null || "".equals(enc) ? System
				.getProperty("file.encoding") : enc;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		if (!(request instanceof HttpServletRequest)) {
			chain.doFilter(request, response);
			return;
		}
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		if (!ServletFileUpload.isMultipartContent(httpRequest)) {
			chain.doFilter(request, response);
			return;
		}
		DiskFileItemFactory factory = new DiskFileItemFactory();
		factory.setRepository(new File(this.tmpDirectoryPath));
		ServletFileUpload upload = new ServletFileUpload(factory);
		Map map = new HashMap();
		List items = null;
		try {
			items = upload.parseRequest(httpRequest);
		} catch (FileUploadException e) {
			ServletException se = new ServletException();
			se.initCause(e);
			throw se;
		}
		for (FileItem fileItem : items) {
			if (fileItem.isFormField()) {
				processFormField(fileItem, map);
			} else {
				processFileField(fileItem, httpRequest);
			}
		}
		request = wrapRequestToExcludeFileFromParamters(httpRequest, map);
		chain.doFilter(request, response);
	}

	private void processFileField(FileItem fileItem, HttpServletRequest request) {
		request.setAttribute(fileItem.getFieldName(), fileItem);
	}

	private void processFormField(FileItem fileItem, Map map)
			throws ServletException {
		String name = fileItem.getFieldName();
		String value;
		try {
			value = fileItem.getString(this.encoding);
		} catch (UnsupportedEncodingException e) {
			throw new ServletException(e);
		}
		String[] values = map.get(name);
		if (values == null) {
			map.put(name, new String[] { value });
			return;
		}
		int l = values.length;
		String[] newValues = new String[l + 1];
		System.arraycopy(values, 0, newValues, 0, l);
		newValues[l] = value;
		map.put(name, newValues);
	}

	private HttpServletRequest wrapRequestToExcludeFileFromParamters(
			HttpServletRequest request, final Map parameterMap) {
		// パラメータにアップロードされたファイルを含めないように, リクエストオブジェクトをラップする.
		return new HttpServletRequestWrapper(request) {
			@Override
			public Map getParameterMap() {
				return parameterMap;
			}

			@Override
			public String[] getParameterValues(String name) {
				return parameterMap.get(name);
			}

			@Override
			public String getParameter(String name) {
				String[] parameters = getParameterValues(name);
				return parameters == null ? null : parameters[0];
			}

			@Override
			public Enumeration getParameterNames() {
				return Collections.enumeration(parameterMap.keySet());
			}
		};
	}

	@Override
	public void destroy() {
		// nop
	}
}

つづいて、JSFコンポーネント

import java.util.Collection;
import javax.el.ValueExpression;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
import FileUploadRenderer;
import org.apache.commons.fileupload.FileItem;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

@FacesComponent("fileUpload")
public class HtmlFileUpload extends UIInput implements ClientBehaviorHolder {
	private static final java.util.Collection CLIENT_EVENTS_LIST = java.util.Collections
			.unmodifiableCollection(java.util.Arrays.asList("valueChange"));

	protected enum PropertyKeys {
		onchange, size
	}

	@Override
	public String getRendererType() {
		return FileUploadRenderer.class.getName();
	}

	@Override
	public String getFamily() {
		return "FileUpload";
	}

	@Override
	public Collection getEventNames() {
		return CLIENT_EVENTS_LIST;
	}

	@Override
	public String getDefaultEventName() {
		return "";
	}

	@Override
	public void addClientBehavior(String eventName, ClientBehavior behavior) { 
            // no-op
	}

	@Override
	public void decode(FacesContext context) {
		HttpServletRequest request = (HttpServletRequest) context
				.getExternalContext().getRequest();
		String clientId = getClientId(context);
		/*
		 * FileUploadFilterでリクエストにアップロードファイルをセットしているため、
		 * エレメントのIDでリクエストから取り出す。
		 */
		FileItem fileItem = (FileItem) request.getAttribute(clientId);
		ValueExpression expression = getValueExpression("value");
		if (expression != null && fileItem != null) {
			UIComponent component = getCurrentComponent(context);
			EditableValueHolder holder = (EditableValueHolder) component;
			/*
			 * Commons FileUploadで処理されたアップロードファイルを
                         * SpringのMultipartFileでラップして、
			 * アプリケーション側で扱う。
			 */
			holder.setSubmittedValue(new CommonsMultipartFile(fileItem));
			holder.setValid(true);
		}
	}

	public String getOnchange() {
		return (String) getStateHelper().eval(PropertyKeys.onchange);
	}

	public void setOnchange(String onchange) {
		getStateHelper().put(PropertyKeys.onchange, onchange);
	}

	public Long getSize() {
		return (Long) getStateHelper().eval(PropertyKeys.size);
	}

	public void setSize(Long size) {
		getStateHelper().put(PropertyKeys.size, size);
	}
}

JSFコンポーネントのレンダラ。
JSFの設定ファイルか、@FacesRendererで定義する。

import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.apache.commons.lang.StringUtils;

public class FileUploadRenderer extends HtmlRenderer {

    @Override
    public void encodeEnd(FacesContext context, UIComponent component)
            throws IOException {

        ResponseWriter writer = context.getResponseWriter();
        final String name = "input";

        writer.startElement(name, null);
        String clientId = component.getClientId();
        writer.writeAttribute("id", clientId, "id");
        writer.writeAttribute("name", clientId, "clientId");
        writer.writeAttribute("type", "file", "file");

        String styleClass = (String) component
            .getAttributes()
            .get("styleClass");
        if (StringUtils.isNotBlank(styleClass)) {
            writer.writeAttribute("class", styleClass, "styleClass");
        }

        String style = (String) component.getAttributes().get("style");
        if (StringUtils.isNotBlank(style)) {
            writer.writeAttribute("style", style, "style");
        }

        writer.endElement(name);
        writer.flush();
    }

}

JSFの設定ファイルなら、こんな感じで定義する。

<renderer>
  <component-family>net.kronosjp.jyukutyo.FileUpload</component-family>
  <renderer-type>net.kronosjp.jyukutyo.core.api.web.render.FileUploadRenderer</renderer-type>
  <renderer-class>net.kronosjp.jyukutyo.core.api.web.render.FileUploadRenderer</renderer-class>
</renderer>

Faceletsの設定ファイルで、タグを定義する。

<tag>
  <tag-name>fileUpload</tag-name>
  <component>
    <component-type>fileUpload</component-type>
   </component>
</tag>

これで完了。ファイルサイズをバリデートする場合、Validatorを作る。

import java.util.Arrays;
import java.util.List;

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;

import org.apache.commons.lang.StringUtils;
import org.springframework.web.multipart.MultipartFile;

/**
 * アップロードファイルバリデータ。
 * 
 */
@FacesValidator(value = "net.kronosjp.jyukutyo.fileUploadValidator")
public class UploadedFileValidator implements Validator {

    // 200kb
    private static final Long DEFAULT_MAX_FILE_SIZE = 200000L;

    public static final String NOT_FOUND_MESSAGE_ID = "net.kronosjp.jyukutyo.fileUploadValidator.NOT_FOUND";
    public static final String NOT_MATCHED_MESSAGE_ID = "net.kronosjp.jyukutyo.fileUploadValidator.NOT_MATCHED";
    public static final String SIZE_OVER_MESSAGE_ID = "net.kronosjp.jyukutyo.fileUploadValidator.SIZE_OVER";
    // 設定最大サイズ
    private Long size;

    /** 有効な拡張子 */
    private static final List<String> VALID_EXTENSIONS = Arrays.asList(new String[] { "jpg", "jpeg" });

    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        if (value == null) {
            return;
        }
        final Long maxSize;
        if (this.size == null || this.size <= 0) {
            maxSize = DEFAULT_MAX_FILE_SIZE;
        } else {
            maxSize = this.size;
        }

        MultipartFile file = (MultipartFile) value;
        if (file.isEmpty() && StringUtils.isNotEmpty(file.getOriginalFilename())) {
            throw new ValidatorException("ファイルがありません");
        } else if (file.isEmpty()) {
            return;
        }
        String extension =
                file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".") + 1).toLowerCase();

        if (!VALID_EXTENSIONS.contains(extnsion)) {
            throw new ValidatorException("有効な拡張子ではありません");
        }

        if (maxSize < file.getSize()) {
            throw new ValidatorException("サイズオーバーです");
        }
    }

    public Long getSize() {
        return this.size;
    }

    public void setSize(Long size) {
        this.size = size;
    }
}