All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.omnifaces.component.input.InputFile Maven / Gradle / Ivy

There is a newer version: 4.5.1
Show newest version
/*
 * Copyright OmniFaces
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.omnifaces.component.input;

import static java.lang.Boolean.FALSE;
import static java.lang.String.format;
import static java.util.Collections.singleton;
import static java.util.Collections.unmodifiableList;
import static javax.faces.application.ResourceHandler.JSF_SCRIPT_LIBRARY_NAME;
import static javax.faces.application.ResourceHandler.JSF_SCRIPT_RESOURCE_NAME;
import static javax.faces.component.behavior.ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME;
import static org.omnifaces.config.OmniFaces.OMNIFACES_EVENT_PARAM_NAME;
import static org.omnifaces.config.OmniFaces.OMNIFACES_LIBRARY_NAME;
import static org.omnifaces.config.OmniFaces.OMNIFACES_SCRIPT_NAME;
import static org.omnifaces.config.OmniFaces.getMessage;
import static org.omnifaces.el.functions.Numbers.formatBytes;
import static org.omnifaces.util.Ajax.update;
import static org.omnifaces.util.Components.getMessageComponent;
import static org.omnifaces.util.Components.getMessagesComponent;
import static org.omnifaces.util.Components.validateHasParent;
import static org.omnifaces.util.Faces.getLocale;
import static org.omnifaces.util.Faces.isDevelopment;
import static org.omnifaces.util.Faces.isRenderResponse;
import static org.omnifaces.util.FacesLocal.getMimeType;
import static org.omnifaces.util.FacesLocal.getRequestParameter;
import static org.omnifaces.util.FacesLocal.getRequestParts;
import static org.omnifaces.util.FacesLocal.isAjaxRequest;
import static org.omnifaces.util.Messages.addError;
import static org.omnifaces.util.Servlets.getSubmittedFileName;
import static org.omnifaces.util.Utils.coalesce;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.faces.application.ResourceDependency;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.component.html.HtmlInputFile;
import javax.faces.context.FacesContext;
import javax.servlet.http.Part;

import org.omnifaces.util.Components;
import org.omnifaces.util.Servlets;
import org.omnifaces.util.State;
import org.omnifaces.util.Utils;

/**
 * 

* The <o:inputFile> is a component that extends the standard <h:inputFile> and * adds support for multiple, directory, accept and maxsize * attributes, along with built-in server side validation on accept and maxsize attributes. * Additionally, it makes sure that the value of HTML file input element is never rendered. The standard * <h:inputFile> renders Part#toString() to it which is unnecessary. * *

Usage

*

* You can use it the same way as <h:inputFile>, you only need to change h: into * o: to get the extra support for multiple, directory and accept * attributes. Here's are some usage examples. * *

Single file selection

*

* It is basically not different from <h:inputFile>. You might as good use it instead. *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile value="#{bean.file}" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 * </h:form>
 * 
*
 * private Part file; // +getter+setter
 *
 * public void upload() {
 *     if (file != null) {
 *         String name = Servlets.getSubmittedFileName(file);
 *         String type = file.getContentType();
 *         long size = file.getSize();
 *         InputStream content = file.getInputStream();
 *         // ...
 *     }
 * }
 * 
*

* Note that it's strongly recommended to use {@link Servlets#getSubmittedFileName(Part)} to obtain the submitted file * name to make sure that any path is stripped off. Some browsers are known to incorrectly include the client side path * or even a fake path along with the file name. * *

Multiple file selection

*

* The multiple attribute can be set to true to enable multiple file selection. * With this setting the enduser can use control/command/shift keys to select multiple files. *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile value="#{bean.files}" multiple="true" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 * </h:form>
 * 
*
 * private List<Part> files; // +getter+setter
 *
 * public void upload() {
 *     if (files != null) {
 *         for (Part file : files) {
 *             String name = Servlets.getSubmittedFileName(file);
 *             String type = file.getContentType();
 *             long size = file.getSize();
 *             InputStream content = file.getInputStream();
 *             // ...
 *         }
 *     }
 * }
 * 
* *

Folder selection

*

* The directory attribute can be set to true to enable folder selection. This implicitly also * sets multiple attribute to true and renders an additional webkitdirectory * attribute to HTML for better browser compatibility. *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile value="#{bean.files}" directory="true" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 * </h:form>
 * 
*
 * private List<Part> files; // +getter+setter
 *
 * public void upload() {
 *     if (files != null) {
 *         for (Part file : files) {
 *             String name = Servlets.getSubmittedFileName(file);
 *             String type = file.getContentType();
 *             long size = file.getSize();
 *             InputStream content = file.getInputStream();
 *             // ...
 *         }
 *     }
 * }
 * 
*

* Do note that this does not send physical folders, but only files contained in those folders. * *

Media type filtering

*

* The accept attribute can be set with a comma separated string of media types of files to filter in * browse dialog. An overview of all registered media types can be found at * IANA. *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.losslessImageFile}" accept="image/png,image/gif" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message for="file" />
 * </h:form>
 * 
*
 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.anyImageFile}" accept="image/*" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message for="file" />
 * </h:form>
 * 
*
 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.anyMediaFile}" accept="audio/*,image/*,video/*" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message for="file" />
 * </h:form>
 * 
*

* This will also be validated in the server side using a built-in validator. Do note that the accept * attribute only filters in client side and validates in server side based on the file extension, and this does thus * not strictly validate the file's actual content. To cover that as well, you should in the bean's action method parse * the file's actual content using the tool suited for the specific media type, such as ImageIO#read() for * image files, and then checking if it returns the expected result. *

* The default message for server side validation of accept attribute is: *

{0}: Media type of file ''{1}'' does not match ''{2}''
*

* Where {0} is the component's label and {1} is the submitted file name and {2} * is the value of accept attribute. *

* You can override the default message by the acceptMessage attribute: *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.anyImageFile}" accept="image/*" acceptMessage="File {1} is unacceptable!" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message for="file" />
 * </h:form>
 * 
*

* Or by the custom message bundle file as identified by <application><message-bundle> in * faces-config.xml. The message key is org.omnifaces.component.input.InputFile.accept. *

 * org.omnifaces.component.input.InputFile.accept = File {1} is unacceptable!
 * 
* *

File size validation

*

* The maxsize attribute can be set with the maximum file size in bytes which will be validated on each * selected file in the client side if the client supports HTML5 File API. This validation will be performed by custom * JavaScript in client side instead of by JSF in server side. This only requires that there is a * <h:message> or <h:messages> component and that it has its id set. *

 * <o:inputFile id="file" ... />
 * <h:message id="messageForFile" for="file" /> <!-- This must have 'id' attribute set! -->
 * 
*

* This way the client side can trigger JSF via an ajax request to update the message component with the client side * validation message. Noted should be that the file(s) will not be sent, hereby saving network * bandwidth. *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.file}" maxsize="#{10 * 1024 * 1024}" /> <!-- 10MiB -->
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message id="messageForFile" for="file" />
 * </h:form>
 * 
*

* This will also be validated in the server side using a built-in validator. *

* The default message for both client side and server side validation of maxsize attribute is: *

{0}: Size of file ''{1}'' is larger than maximum of {2}
*

* Where {0} is the component's label and {1} is the submitted file name and {2} * is the value of maxsize attribute. *

* You can override the default message by the maxsizeMessage attribute: *

 * <h:form enctype="multipart/form-data">
 *     <o:inputFile id="file" value="#{bean.file}" maxsize="#{10 * 1024 * 1024}" maxsizeMessage="File {1} is too big!" />
 *     <h:commandButton value="Upload" action="#{bean.upload}" />
 *     <h:message id="messageForFile" for="file" />
 * </h:form>
 * 
*

* Or by the custom message bundle file as identified by <application><message-bundle> in * faces-config.xml. The message key is org.omnifaces.component.input.InputFile.maxsize. *

 * org.omnifaces.component.input.InputFile.maxsize = File {1} is too big!
 * 
* * @author Bauke Scholtz * @since 2.5 */ @FacesComponent(InputFile.COMPONENT_TYPE) @ResourceDependency(library=JSF_SCRIPT_LIBRARY_NAME, name=JSF_SCRIPT_RESOURCE_NAME, target="head") // Required for jsf.ajax.request. @ResourceDependency(library=OMNIFACES_LIBRARY_NAME, name=OMNIFACES_SCRIPT_NAME, target="head") // Specifically inputfile.js. public class InputFile extends HtmlInputFile { // Public constants ----------------------------------------------------------------------------------------------- public static final String COMPONENT_TYPE = "org.omnifaces.component.input.InputFile"; // Private constants ---------------------------------------------------------------------------------------------- private static final String SCRIPT_ONCHANGE = "if(!OmniFaces.InputFile.validate(event,this,'%s',%s))return false;%s"; private static final String ERROR_MISSING_MESSAGE_COMPONENT = "o:inputFile client side validation of maxsize requires a message(s) component with a fixed ID."; private enum PropertyKeys { // Cannot be uppercased. They have to exactly match the attribute names. multiple, directory, accept, acceptMessage, maxsize, maxsizeMessage; } // Variables ------------------------------------------------------------------------------------------------------ private final State state = new State(getStateHelper()); private transient Object transientSubmittedValue; private String messageComponentClientId; // Actions -------------------------------------------------------------------------------------------------------- /** * This override checks if client side validation on maxsize has failed and if multi file upload is enabled. * If client side validation on maxsize has failed, then it will render the message. If multi file upload is * enabled, then it will set all parts as submitted value instead of only the last part as done in h:inputFile. */ @Override public void decode(FacesContext context) { if ("validationFailed".equals(getRequestParameter(context, OMNIFACES_EVENT_PARAM_NAME)) && getClientId(context).equals(getRequestParameter(context, BEHAVIOR_SOURCE_PARAM_NAME))) { String fileName = getRequestParameter(context, "fileName"); addError(getClientId(context), getMaxsizeMessage(), Components.getLabel(this), fileName, formatBytes(getMaxsize())); setValid(false); context.validationFailed(); update(getMessageComponentClientId()); context.renderResponse(); } else { super.decode(context); Object submittedValue = getSubmittedValue(); if (submittedValue instanceof Part && isMultiple()) { setSubmittedValue(getRequestParts(context, ((Part) submittedValue).getName())); } } } /** * This override will convert the individual parts if multi file upload is enabled and collect only non-null parts * having a non-empty file name and a file size above zero. */ @Override @SuppressWarnings("unchecked") protected Object getConvertedValue(FacesContext context, Object submittedValue) { if (submittedValue == null) { return null; } if (isMultiple()) { List convertedParts = new ArrayList<>(); for (Part submittedPart : (List) submittedValue) { Object convertedPart = super.getConvertedValue(context, submittedPart); if (convertedPart instanceof Part && !Utils.isEmpty(convertedPart)) { // Do not import static! UIInput has an isEmpty() as well. convertedParts.add((Part) convertedPart); } } return unmodifiableList(convertedParts); } Object convertedPart = super.getConvertedValue(context, submittedValue); return Utils.isEmpty(convertedPart) ? null : convertedPart; } /** * This override will server-side validate any accept and maxsize for each part. */ @Override @SuppressWarnings("unchecked") protected void validateValue(FacesContext context, Object convertedValue) { Collection convertedParts = null; if (convertedValue instanceof Part) { convertedParts = singleton((Part) convertedValue); } else if (convertedValue instanceof List) { convertedParts = (List) convertedValue; } if (convertedParts != null) { validateParts(context, convertedParts); } if (isValid()) { super.validateValue(context, convertedValue); } else if (isAjaxRequest(context)) { update(getMessageComponentClientId()); } } /** * This override returns null during render response as it doesn't make sense to render Part#toString() * as value of file input, moreover it's for HTML security reasons discouraged to prefill the value of a file input * even though browsers will ignore it. */ @Override public Object getValue() { return isRenderResponse() ? null : super.getValue(); } /** * This override will render multiple, directory and accept attributes * accordingly. As the directory attribute is relatively new, for better browser compatibility the * webkitdirectory attribute will also be written along it. *

* They're written as passthrough attributes because in Mojarra the startElement() takes place in * {@link #encodeEnd(FacesContext)} instead of {@link #encodeBegin(FacesContext)}. */ @Override public void encodeEnd(FacesContext context) throws IOException { Map passThroughAttributes = getPassThroughAttributes(); if (isMultiple()) { passThroughAttributes.put("multiple", true); // https://caniuse.com/#feat=input-file-multiple } if (isDirectory()) { passThroughAttributes.put("directory", true); // Firefox 46+ (Firefox 42-45 requires enabling via about:config). passThroughAttributes.put("webkitdirectory", true); // Chrome 11+, Safari 4+ and Edge. } String accept = getAccept(); if (accept != null) { passThroughAttributes.put("accept", accept); // https://caniuse.com/#feat=input-file-accept } Long maxsize = getMaxsize(); if (maxsize != null) { validateHierarchy(); setOnchange(format(SCRIPT_ONCHANGE, getMessageComponentClientId(), maxsize, coalesce(getOnchange(), ""))); } super.encodeEnd(context); } /** * Validate the component hierarchy. This should only be called when project stage is Development. * @throws IllegalStateException When component hierarchy is wrong. */ protected void validateHierarchy() { validateHasParent(this, UIForm.class); if (isDevelopment() && getMessageComponentClientId() == null) { throw new IllegalStateException(ERROR_MISSING_MESSAGE_COMPONENT); } } // Attribute getters/setters -------------------------------------------------------------------------------------- /** * An override which ensures that the Faces implementation being used doesn't save it in the state. * The {@link Part} does namely not belong there. */ @Override public Object getSubmittedValue() { return transientSubmittedValue; } /** * An override which ensures that the Faces implementation being used doesn't save it in the state. * The {@link Part} does namely not belong there. */ @Override public void setSubmittedValue(Object submittedValue) { this.transientSubmittedValue = submittedValue; } /** * Returns whether or not to allow multiple file selection. * This implicitly defaults to true when directory attribute is true. * @return Whether or not to allow multiple file selection. */ public boolean isMultiple() { return state.get(PropertyKeys.multiple, isDirectory()); } /** * Sets whether or not to allow multiple file selection. * @param multiple Whether or not to allow multiple file selection. */ public void setMultiple(boolean multiple) { state.put(PropertyKeys.multiple, multiple); } /** * Returns whether or not to enable directory selection. * @return Whether or not to enable directory selection. */ public boolean isDirectory() { return state.get(PropertyKeys.directory, FALSE); } /** * Sets whether or not to enable directory selection. * When true, this implicitly defaults the multiple attribute to true. * @param directory Whether or not to enable directory selection. */ public void setDirectory(boolean directory) { state.put(PropertyKeys.directory, directory); } /** * Returns comma separated string of mime types of files to filter in client side file browse dialog. * This is also validated in server side. * @return Comma separated string of mime types of files to filter in client side file browse dialog. */ public String getAccept() { return state.get(PropertyKeys.accept); } /** * Sets comma separated string of media types of files to filter in client side file browse dialog. * @param accept Comma separated string of mime types of files to filter in client side file browse dialog. */ public void setAccept(String accept) { state.put(PropertyKeys.accept, accept); } /** * Returns validation message to be displayed when the condition in accept attribute is violated. * @return Validation message to be displayed when the condition in accept attribute is violated. */ public String getAcceptMessage() { return state.get(PropertyKeys.acceptMessage, getMessage(COMPONENT_TYPE + ".accept")); } /** * Sets validation message to be displayed when the condition in accept attribute is violated. * @param acceptMessage Validation message to be displayed when the condition in accept attribute is * violated. */ public void setAcceptMessage(String acceptMessage) { state.put(PropertyKeys.acceptMessage, acceptMessage); } /** * Returns maximum size in bytes for each selected file. * This is validated in both client and server side. * @return Maximum size in bytes for each selected file. */ public Long getMaxsize() { return state.get(PropertyKeys.maxsize); } /** * Sets maximum size in bytes for each selected file. * @param maxsize Maximum size in bytes for each selected file. */ public void setMaxsize(Long maxsize) { state.put(PropertyKeys.maxsize, maxsize); } /** * Returns validation message to be displayed when the condition in maxsize attribute is violated. * @return Validation message to be displayed when the condition in maxsize attribute is violated. */ public String getMaxsizeMessage() { return state.get(PropertyKeys.maxsizeMessage, getMessage(COMPONENT_TYPE + ".maxsize")); } /** * Sets validation message to be displayed when the condition in maxsize attribute is violated. * @param maxsizeMessage Validation message to be displayed when the condition in maxsize attribute is * violated. */ public void setMaxsizeMessage(String maxsizeMessage) { state.put(PropertyKeys.maxsizeMessage, maxsizeMessage); } // Helpers -------------------------------------------------------------------------------------------------------- private void validateParts(FacesContext context, Collection parts) { String accept = getAccept(); Long maxsize = getMaxsize(); if (accept == null && maxsize == null) { return; } for (Part part : parts) { validatePart(context, part, accept, maxsize); } } private void validatePart(FacesContext context, Part part, String accept, Long maxsize) { String fileName = getSubmittedFileName(part); String message = null; String param = null; if (accept != null) { String contentType = isEmpty(fileName) ? part.getContentType() : getMimeType(context, fileName.toLowerCase(getLocale())); if (contentType == null || !contentType.matches(convertAcceptToRegex(accept))) { message = getAcceptMessage(); param = accept; } } if (message == null && maxsize != null && part.getSize() > maxsize) { message = getMaxsizeMessage(); param = formatBytes(maxsize); } if (message != null) { addError(getClientId(context), message, Components.getLabel(this), fileName, param); setValid(false); } } private String convertAcceptToRegex(String accept) { String[] parts = accept.replaceAll("\\s*", "").split("(?<=[*,])|(?=[*,])"); StringBuilder regex = new StringBuilder(); for (String part : parts) { switch (part) { case "*": regex.append(".*"); break; case ",": regex.append("|"); break; default: regex.append(Pattern.quote(part)); break; } } return regex.toString(); } private String getMessageComponentClientId() { if (messageComponentClientId != null) { return messageComponentClientId; } UIComponent component = getMessageComponent(this); if (component == null || component.getId() == null) { component = getMessagesComponent(); } messageComponentClientId = (component != null && component.getId() != null) ? component.getClientId() : null; return messageComponentClientId; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy