Skip to content

Commit

Permalink
#235: added o:inputFile along with bunch of Part utility
Browse files Browse the repository at this point in the history
(todo: document omnifaces-ui.taglib.xml)
  • Loading branch information
Bauke Scholtz committed Jul 17, 2016
1 parent a842877 commit 437426a
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 6 deletions.
141 changes: 141 additions & 0 deletions src/main/java/org/omnifaces/component/input/InputFile.java
@@ -0,0 +1,141 @@
/*
* Copyright 2016 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
*
* http://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 org.omnifaces.util.Faces.isRenderResponse;
import static org.omnifaces.util.FacesLocal.getRequestParts;
import static org.omnifaces.util.Renderers.writeAttribute;

import java.io.IOException;

import javax.faces.component.FacesComponent;
import javax.faces.component.html.HtmlInputFile;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.ConverterException;
import javax.servlet.http.Part;

import org.omnifaces.util.State;

/**
* <p>
* The <code>&lt;o:inputFile&gt;</code> is a component that extends the standard <code>&lt;h:inputFile&gt;</code> and
* adds support for <code>multiple</code> and <code>directory</code> attributes.
*
* @author Bauke Scholtz
* @since 2.5
*/
@FacesComponent(InputFile.COMPONENT_TYPE)
public class InputFile extends HtmlInputFile {

// Public constants -----------------------------------------------------------------------------------------------

public static final String COMPONENT_TYPE = "org.omnifaces.component.input.InputFile";

// Private constants ----------------------------------------------------------------------------------------------

private enum PropertyKeys {
// Cannot be uppercased. They have to exactly match the attribute names.
multiple, directory;
}

// Variables ------------------------------------------------------------------------------------------------------

private final State state = new State(getStateHelper());

// Actions --------------------------------------------------------------------------------------------------------

/**
* This override checks if multi file upload was enabled and if so, then return a collection of parts instead of
* only the last part as done in h:inputFile.
*/
@Override
protected Object getConvertedValue(FacesContext context, Object newSubmittedValue) throws ConverterException {
Object convertedValue = super.getConvertedValue(context, newSubmittedValue);

if ((convertedValue instanceof Part) && isMultiple()) {
return getRequestParts(context, ((Part) convertedValue).getName());
}

return convertedValue;
}

/**
* This override returns null during render response as it doesn't make sense to print {@link Part#toString()} as
* value of file input, moreover it's for HTML security reasons disallowed 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 <code>multiple</code> and <code>directory</code> attributes accordingly. As the
* <code>directory</code> attribute is relatively new, for better compatibility the <code>webkitdirectory</code>
* attribute will also be written.
*/
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();

if (isMultiple()) {
writeAttribute(writer, this, "multiple"); // http://caniuse.com/#feat=input-file-multiple
}

if (isDirectory()) {
writeAttribute(writer, this, "directory"); // Firefox 46+ (Firefox 42-45 requires enabling via about:config).
writeAttribute(writer, this, "directory", "webkitdirectory"); // Chrome 11+, Safari 4+ and Edge.
}

super.encodeEnd(context);
}

// Attribute getters/setters --------------------------------------------------------------------------------------

/**
* Returns whether or not to allow multiple file selection.
* This implicitly returns true when directory selection is enabled.
* @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.
* Do note that this does not send physical folders, but only files contained in those folders.
* @param directory Whether or not to enable directory selection.
*/
public void setDirectory(boolean directory) {
state.put(PropertyKeys.directory, directory);
}

}
42 changes: 42 additions & 0 deletions src/main/java/org/omnifaces/util/Faces.java
Expand Up @@ -32,6 +32,7 @@
import javax.el.ELResolver;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
Expand All @@ -56,6 +57,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;

import org.omnifaces.component.ParamHolder;
import org.omnifaces.config.FacesConfigXml;
Expand Down Expand Up @@ -986,6 +988,46 @@ public static String[] getRequestParameterValues(String name) {
return FacesLocal.getRequestParameterValuesMap(getContext()).get(name);
}

/**
* Returns all HTTP request parts, provided that request is of type <code>multipart/form-data</code>. If there are
* no parts, an empty collection is returned.
* @return all HTTP request parts.
* @throws FacesException Whenever something fails at servlet or I/O level. The caller should preferably not catch
* it, but just let it go. The servletcontainer will handle it.
* @see HttpServletRequest#getParts()
* @since 2.5
*/
public static Collection<Part> getRequestParts() {
return FacesLocal.getRequestParts(getContext());
}

/**
* Returns the HTTP request part associated with the given name, else return null.
* @param name The HTTP request part name.
* @return The HTTP request part associated with the given name.
* @throws FacesException Whenever something fails at servlet or I/O level. The caller should preferably not catch
* it, but just let it go. The servletcontainer will handle it.
* @see HttpServletRequest#getPart(String)
* @since 2.5
*/
public static Part getRequestPart(String name) {
return FacesLocal.getRequestPart(getContext(), name);
}

/**
* Returns all HTTP request parts associated with the given name, provided that request is of type
* <code>multipart/form-data</code>. If there are no parts, an empty collection is returned.
* @param name The HTTP request part name.
* @return All HTTP request parts associated with the given name.
* @throws FacesException Whenever something fails at servlet or I/O level. The caller should preferably not catch
* it, but just let it go. The servletcontainer will handle it.
* @see HttpServletRequest#getParts()
* @since 2.5
*/
public static Collection<Part> getRequestParts(String name) {
return FacesLocal.getRequestParts(getContext(), name);
}

/**
* Returns the HTTP request header map.
* @return The HTTP request header map.
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/org/omnifaces/util/FacesLocal.java
Expand Up @@ -13,6 +13,7 @@
package org.omnifaces.util;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_PERMANENTLY;
import static org.omnifaces.util.Reflection.instance;
import static org.omnifaces.util.Reflection.toClassOrNull;
Expand Down Expand Up @@ -47,6 +48,7 @@
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ProjectStage;
Expand All @@ -71,6 +73,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;

import org.omnifaces.component.ParamHolder;
import org.omnifaces.config.FacesConfigXml;
Expand Down Expand Up @@ -799,6 +802,53 @@ public static String[] getRequestParameterValues(FacesContext context, String na
return getRequestParameterValuesMap(context).get(name);
}

/**
* {@inheritDoc}
* @see Faces#getRequestParts()
*/
public static Collection<Part> getRequestParts(FacesContext context) {
try {
return getRequest(context).getParts();
}
catch (IOException | ServletException e) {
throw new FacesException(e);
}
}

/**
* {@inheritDoc}
* @see Faces#getRequestPart(String)
*/
public static Part getRequestPart(FacesContext context, String name) {
try {
return getRequest(context).getPart(name);
}
catch (ServletException | IOException e) {
throw new FacesException(e);
}
}

/**
* {@inheritDoc}
* @see Faces#getRequestParts(String)
*/
public static Collection<Part> getRequestParts(FacesContext context, String name) {
try {
List<Part> parts = new ArrayList<>();

for (Part part : getRequest(context).getParts()) {
if (name.equals(part.getName())) {
parts.add(part);
}
}

return unmodifiableList(parts);
}
catch (ServletException | IOException e) {
throw new FacesException(e);
}
}

/**
* {@inheritDoc}
* @see Faces#getRequestHeaderMap()
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/omnifaces/util/Servlets.java
Expand Up @@ -48,6 +48,7 @@
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

import org.omnifaces.component.ParamHolder;

Expand Down Expand Up @@ -390,6 +391,24 @@ public static String getRemoteAddr(HttpServletRequest request) {
return isEmpty(forwardedFor) ? request.getRemoteAddr() : forwardedFor.split("\\s*,\\s*", 2)[0]; // It's a comma separated string: client,proxy1,proxy2,...
}

/**
* Returns the submitted file name of the given part, making sure that any path is stripped off. Some browsers
* are known to incorrectly include the client side path along with it.
* @param part The part of a multipart/form-data request.
* @return The submitted file name of the given part, or null if there is none.
* @since 2.5
*/
public static String getSubmittedFileName(Part part) {
for (String cd : part.getHeader("Content-Disposition").split("\\s*;\\s*")) {
if (cd.startsWith("filename")) {
String fileName = cd.substring(cd.indexOf('=') + 1).trim().replace("\"", "");
return fileName.substring(fileName.lastIndexOf('/') + 1).substring(fileName.lastIndexOf('\\') + 1); // MSIE fix.
}
}

return null;
}

// HttpServletResponse --------------------------------------------------------------------------------------------

/**
Expand Down
30 changes: 24 additions & 6 deletions src/main/resources/META-INF/omnifaces-ui.taglib.xml
Expand Up @@ -1766,6 +1766,24 @@ public class ValidateValuesBean implements MultiFieldValidator {
</attribute>
</tag>

<tag>
<description>TBD</description>
<tag-name>inputFile</tag-name>
<component>
<component-type>org.omnifaces.component.input.InputFile</component-type>
</component>
<attribute>
<name>multiple</name>
<required>false</required>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<name>directory</name>
<required>false</required>
<type>java.lang.Boolean</type>
</attribute>
</tag>

<tag>
<description>
<![CDATA[
Expand Down Expand Up @@ -2847,7 +2865,7 @@ public class ValidateValuesBean implements MultiFieldValidator {
</description>
<name>dataURI</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
Expand Down Expand Up @@ -2940,7 +2958,7 @@ public class ValidateValuesBean implements MultiFieldValidator {
</description>
<name>ismap</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
Expand Down Expand Up @@ -3182,7 +3200,7 @@ public class ValidateValuesBean implements MultiFieldValidator {
</description>
<name>includeViewParams</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
Expand All @@ -3194,7 +3212,7 @@ public class ValidateValuesBean implements MultiFieldValidator {
</description>
<name>includeRequestParams</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
</tag>

Expand Down Expand Up @@ -3235,15 +3253,15 @@ public class ValidateValuesBean implements MultiFieldValidator {
</description>
<name>includeRequestParams</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
Whether to use current request URI with query string as action URI. This overrides includeViewParams and includeRequestParams.
</description>
<name>useRequestURI</name>
<required>false</required>
<type>boolean</type>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
Expand Down

0 comments on commit 437426a

Please sign in to comment.