You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2014/07/20 11:04:51 UTC
git commit: WW-3025 Adds new JakartaStream multipart request handler
It solves problem with loosing parameters during large files which exceeds
upload file limit
Repository: struts
Updated Branches:
refs/heads/develop de5edd875 -> e7a414fea
WW-3025 Adds new JakartaStream multipart request handler
It solves problem with loosing parameters during large files which
exceeds upload file limit
Project: http://git-wip-us.apache.org/repos/asf/struts/repo
Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/e7a414fe
Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/e7a414fe
Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/e7a414fe
Branch: refs/heads/develop
Commit: e7a414fea354ccf6694f3d101d53f5ff311c69d2
Parents: de5edd8
Author: Lukasz Lenart <lu...@apache.org>
Authored: Sun Jul 20 11:04:41 2014 +0200
Committer: Lukasz Lenart <lu...@apache.org>
Committed: Sun Jul 20 11:04:41 2014 +0200
----------------------------------------------------------------------
.../org/apache/struts2/StrutsConstants.java | 3 +
.../JakartaStreamMultiPartRequest.java | 598 +++++++++++++++++++
.../org/apache/struts2/default.properties | 1 +
core/src/main/resources/struts-default.xml | 1 +
4 files changed, 603 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/struts/blob/e7a414fe/core/src/main/java/org/apache/struts2/StrutsConstants.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 10deced..7676f4c 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -134,6 +134,9 @@ public final class StrutsConstants {
/** The directory to use for storing uploaded files */
public static final String STRUTS_MULTIPART_SAVEDIR = "struts.multipart.saveDir";
+ /** Declares the buffer size to be used during streaming multipart content to disk. Used only with {@link org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest} */
+ public static final String STRUTS_MULTIPART_BUFFERSIZE = "struts.multipart.bufferSize";
+
/**
* The org.apache.struts2.dispatcher.multipart.MultiPartRequest parser implementation
* for a multipart request (file upload)
http://git-wip-us.apache.org/repos/asf/struts/blob/e7a414fe/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
new file mode 100644
index 0000000..fa3dd2f
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
@@ -0,0 +1,598 @@
+package org.apache.struts2.dispatcher.multipart;
+
+import com.opensymphony.xwork2.LocaleProvider;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.LocalizedTextUtil;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.FileUploadBase;
+import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.util.Streams;
+import org.apache.struts2.StrutsConstants;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Multi-part form data request adapter for Jakarta Commons FileUpload package that
+ * leverages the streaming API rather than the traditional non-streaming API.
+ *
+ * For more details see WW-3025
+ *
+ * @author Chris Cranford
+ * @since 2.3.18
+ */
+public class JakartaStreamMultiPartRequest implements MultiPartRequest {
+
+ static final Logger LOG = LoggerFactory.getLogger(JakartaStreamMultiPartRequest.class);
+
+ /**
+ * Defines the internal buffer size used during streaming operations.
+ */
+ private static final int BUFFER_SIZE = 10240;
+
+ /**
+ * Map between file fields and file data.
+ */
+ private Map<String, List<FileInfo>> fileInfos = new HashMap<String, List<FileInfo>>();
+
+ /**
+ * Map between non-file fields and values.
+ */
+ private Map<String, List<String>> parameters = new HashMap<String, List<String>>();
+
+ /**
+ * Internal list of raised errors to be passed to the the Struts2 framework.
+ */
+ private List<String> errors = new ArrayList<String>();
+
+ /**
+ * Internal list of non-critical messages to be passed to the Struts2 framework.
+ */
+ private List<String> messages = new ArrayList<String>();
+
+ /**
+ * Specifies the maximum size of the entire request.
+ */
+ private Long maxSize;
+
+ /**
+ * Specifies the buffer size to use during streaming.
+ */
+ private int bufferSize = BUFFER_SIZE;
+
+ /**
+ * Localization to be used regarding errors.
+ */
+ private Locale defaultLocale = Locale.ENGLISH;
+
+ /**
+ * Injects the Struts multiple part maximum size.
+ *
+ * @param maxSize
+ */
+ @Inject(StrutsConstants.STRUTS_MULTIPART_MAXSIZE)
+ public void setMaxSize(String maxSize) {
+ this.maxSize = Long.parseLong(maxSize);
+ }
+
+ /**
+ * Sets the buffer size to be used.
+ *
+ * @param bufferSize
+ */
+ @Inject(StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE)
+ public void setBufferSize(String bufferSize) {
+ this.bufferSize = Integer.parseInt(bufferSize);
+ }
+
+ /**
+ * Injects the Struts locale provider.
+ *
+ * @param provider
+ */
+ @Inject
+ public void setLocaleProvider(LocaleProvider provider) {
+ defaultLocale = provider.getLocale();
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
+ */
+ public void cleanUp() {
+ LOG.debug("Performing File Upload temporary storage cleanup.");
+ for (String fieldName : fileInfos.keySet()) {
+ for (FileInfo fileInfo : fileInfos.get(fieldName)) {
+ File file = fileInfo.getFile();
+ LOG.debug("Deleting file '#0'.", file.getName());
+ if (!file.delete())
+ LOG.warn("There was a problem attempting to delete file '#0'.", file.getName());
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
+ */
+ public String[] getContentType(String fieldName) {
+ List<FileInfo> infos = fileInfos.get(fieldName);
+ if (infos == null)
+ return null;
+
+ List<String> types = new ArrayList<String>(infos.size());
+ for (FileInfo fileInfo : infos)
+ types.add(fileInfo.getContentType());
+
+ return types.toArray(new String[types.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
+ */
+ public List<String> getErrors() {
+ return errors;
+ }
+
+ /**
+ * Allows interceptor to fetch non-critical messages that can be passed to the action.
+ *
+ * @return
+ */
+ public List<String> getMesssages() {
+ return messages;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
+ */
+ public File[] getFile(String fieldName) {
+ List<FileInfo> infos = fileInfos.get(fieldName);
+ if (infos == null)
+ return null;
+
+ List<File> files = new ArrayList<File>(infos.size());
+ for (FileInfo fileInfo : infos)
+ files.add(fileInfo.getFile());
+
+ return files.toArray(new File[files.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
+ */
+ public String[] getFileNames(String fieldName) {
+ List<FileInfo> infos = fileInfos.get(fieldName);
+ if (infos == null)
+ return null;
+
+ List<String> names = new ArrayList<String>(infos.size());
+ for (FileInfo fileInfo : infos)
+ names.add(getCanonicalName(fileInfo.getOriginalName()));
+
+ return names.toArray(new String[names.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
+ */
+ public Enumeration<String> getFileParameterNames() {
+ return Collections.enumeration(fileInfos.keySet());
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
+ */
+ public String[] getFilesystemName(String fieldName) {
+ List<FileInfo> infos = fileInfos.get(fieldName);
+ if (infos == null)
+ return null;
+
+ List<String> names = new ArrayList<String>(infos.size());
+ for (FileInfo fileInfo : infos)
+ names.add(fileInfo.getFile().getName());
+
+ return names.toArray(new String[names.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
+ */
+ public String getParameter(String name) {
+ List<String> values = parameters.get(name);
+ if (values != null && values.size() > 0)
+ return values.get(0);
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
+ */
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(parameters.keySet());
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
+ */
+ public String[] getParameterValues(String name) {
+ List<String> values = parameters.get(name);
+ if (values != null && values.size() > 0)
+ return values.toArray(new String[values.size()]);
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(javax.servlet.http.HttpServletRequest, java.lang.String)
+ */
+ public void parse(HttpServletRequest request, String saveDir)
+ throws IOException {
+ try {
+ setLocale(request);
+ processUpload(request, saveDir);
+ } catch (Exception e) {
+ e.printStackTrace();
+ String errorMessage = buildErrorMessage(e, new Object[]{});
+ if (!errors.contains(errorMessage))
+ errors.add(errorMessage);
+ }
+ }
+
+ /**
+ * Inspect the servlet request and set the locale if one wasn't provided by
+ * the Struts2 framework.
+ *
+ * @param request
+ */
+ protected void setLocale(HttpServletRequest request) {
+ if (defaultLocale == null)
+ defaultLocale = request.getLocale();
+ }
+
+ /**
+ * Processes the upload.
+ *
+ * @param request
+ * @param saveDir
+ * @throws Exception
+ */
+ private void processUpload(HttpServletRequest request, String saveDir)
+ throws Exception {
+
+ // Sanity check that the request is a multi-part/form-data request.
+ if (ServletFileUpload.isMultipartContent(request)) {
+
+ // Sanity check on request size.
+ boolean requestSizePermitted = isRequestSizePermitted(request);
+
+ // Interface with Commons FileUpload API
+ // Using the Streaming API
+ ServletFileUpload servletFileUpload = new ServletFileUpload();
+ FileItemIterator i = servletFileUpload.getItemIterator(request);
+
+ // Iterate the file items
+ while (i.hasNext()) {
+ try {
+ FileItemStream itemStream = i.next();
+
+ // If the file item stream is a form field, delegate to the
+ // field item stream handler
+ if (itemStream.isFormField()) {
+ processFileItemStreamAsFormField(itemStream);
+ }
+
+ // Delegate the file item stream for a file field to the
+ // file item stream handler, but delegation is skipped
+ // if the requestSizePermitted check failed based on the
+ // complete content-size of the request.
+ else {
+
+ // prevent processing file field item if request size not allowed.
+ // also warn user in the logs.
+ if (!requestSizePermitted) {
+ addFileSkippedError(itemStream.getName(), request);
+ LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);
+ continue;
+ }
+
+ processFileItemStreamAsFileField(itemStream, saveDir);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Defines whether the request allowed based on content length.
+ *
+ * @param request
+ * @return
+ */
+ private boolean isRequestSizePermitted(HttpServletRequest request) {
+ // if maxSize is specified as -1, there is no sanity check and it's
+ // safe to return true for any request, delegating the failure
+ // checks later in the upload process.
+ if (maxSize == -1 || request == null)
+ return true;
+
+ return request.getContentLength() < maxSize;
+ }
+
+ /**
+ * Get the request content length.
+ *
+ * @param request
+ * @return
+ */
+ private long getRequestSize(HttpServletRequest request) {
+ long requestSize = 0;
+ if (request != null)
+ requestSize = request.getContentLength();
+ return requestSize;
+ }
+
+ /**
+ * Add a file skipped message notification for action messages.
+ *
+ * @param fileName
+ * @param request
+ */
+ private void addFileSkippedError(String fileName, HttpServletRequest request) {
+ String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
+ FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), maxSize);
+ String message = buildMessage(exception, new Object[]{fileName, getRequestSize(request), maxSize});
+ if (!errors.contains(message))
+ errors.add(message);
+ }
+
+ /**
+ * Processes the FileItemStream as a Form Field.
+ *
+ * @param itemStream
+ */
+ private void processFileItemStreamAsFormField(FileItemStream itemStream) {
+ String fieldName = itemStream.getFieldName();
+ try {
+ List<String> values = null;
+ String fieldValue = Streams.asString(itemStream.openStream());
+ if (!parameters.containsKey(fieldName)) {
+ values = new ArrayList<String>();
+ parameters.put(fieldName, values);
+ } else {
+ values = parameters.get(fieldName);
+ }
+ values.add(fieldValue);
+ } catch (IOException e) {
+ e.printStackTrace();
+ LOG.warn("Failed to handle form field '#0'.", fieldName);
+ }
+ }
+
+ /**
+ * Processes the FileItemStream as a file field.
+ *
+ * @param itemStream
+ * @param location
+ */
+ private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
+ File file = null;
+ try {
+ // Create the temporary upload file.
+ file = createTemporaryFile(itemStream.getName(), location);
+
+ if (streamFileToDisk(itemStream, file))
+ createFileInfoFromItemStream(itemStream, file);
+ } catch (IOException e) {
+ if (file != null) {
+ try {
+ file.delete();
+ } catch (SecurityException se) {
+ se.printStackTrace();
+ LOG.warn("Failed to delete '#0' due to security exception above.", file.getName());
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a temporary file based on the given filename and location.
+ *
+ * @param fileName
+ * @param location
+ * @return
+ * @throws IOException
+ */
+ private File createTemporaryFile(String fileName, String location)
+ throws IOException {
+ String name = fileName
+ .substring(fileName.lastIndexOf('/') + 1)
+ .substring(fileName.lastIndexOf('\\') + 1);
+
+ String prefix = name;
+ String suffix = "";
+
+ if (name.contains(".")) {
+ prefix = name.substring(0, name.lastIndexOf('.'));
+ suffix = name.substring(name.lastIndexOf('.'));
+ }
+
+ File file = File.createTempFile(prefix + "_", suffix, new File(location));
+ LOG.debug("Creating temporary file '#0' (originally '#1').", file.getName(), fileName);
+ return file;
+ }
+
+ /**
+ * Streams the file upload stream to the specified file.
+ *
+ * @param itemStream
+ * @param file
+ * @return
+ * @throws IOException
+ */
+ private boolean streamFileToDisk(FileItemStream itemStream, File file) throws IOException {
+ boolean result = false;
+ InputStream input = itemStream.openStream();
+ OutputStream output = null;
+ try {
+ output = new BufferedOutputStream(new FileOutputStream(file), bufferSize);
+ byte[] buffer = new byte[bufferSize];
+ LOG.debug("Streaming file using buffer size #0.", bufferSize);
+ for (int length = 0; ((length = input.read(buffer)) > 0); )
+ output.write(buffer, 0, length);
+ result = true;
+ } finally {
+ if (output != null) {
+ try {
+ output.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates an internal <code>FileInfo</code> structure used to pass information
+ * to the <code>FileUploadInterceptor</code> during the interceptor stack
+ * invocation process.
+ *
+ * @param itemStream
+ * @param file
+ */
+ private void createFileInfoFromItemStream(FileItemStream itemStream, File file) {
+ // gather attributes from file upload stream.
+ String fileName = itemStream.getName();
+ String fieldName = itemStream.getFieldName();
+ // create internal structure
+ FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName);
+ // append or create new entry.
+ if (!fileInfos.containsKey(fieldName)) {
+ List<FileInfo> infos = new ArrayList<FileInfo>();
+ infos.add(fileInfo);
+ fileInfos.put(fieldName, infos);
+ } else {
+ fileInfos.get(fieldName).add(fileInfo);
+ }
+ }
+
+ /**
+ * Get the canonical name based on the supplied filename.
+ *
+ * @param fileName
+ * @return
+ */
+ private String getCanonicalName(String fileName) {
+ int forwardSlash = fileName.lastIndexOf("/");
+ int backwardSlash = fileName.lastIndexOf("\\");
+ if (forwardSlash != -1 && forwardSlash > backwardSlash) {
+ fileName = fileName.substring(forwardSlash + 1, fileName.length());
+ } else {
+ fileName = fileName.substring(backwardSlash + 1, fileName.length());
+ }
+ return fileName;
+ }
+
+ /**
+ * Build error message.
+ *
+ * @param e
+ * @param args
+ * @return
+ */
+ private String buildErrorMessage(Throwable e, Object[] args) {
+ String errorKey = "struts.message.upload.error." + e.getClass().getSimpleName();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Preparing error message for key: [#0]", errorKey);
+ return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
+ }
+
+ /**
+ * Build action message.
+ *
+ * @param e
+ * @param args
+ * @return
+ */
+ private String buildMessage(Throwable e, Object[] args) {
+ String messageKey = "struts.message.upload.message." + e.getClass().getSimpleName();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Preparing message for key: [#0]", messageKey);
+ return LocalizedTextUtil.findText(this.getClass(), messageKey, defaultLocale, e.getMessage(), args);
+ }
+
+ /**
+ * Internal data structure used to store a reference to information needed
+ * to later pass post processing data to the <code>FileUploadInterceptor</code>.
+ *
+ * @version $Revision$
+ * @since 7.0.0
+ */
+ private static class FileInfo implements Serializable {
+
+ private static final long serialVersionUID = 1083158552766906037L;
+
+ private File file;
+ private String contentType;
+ private String originalName;
+
+ /**
+ * Default constructor.
+ *
+ * @param file
+ * @param contentType
+ * @param originalName
+ */
+ public FileInfo(File file, String contentType, String originalName) {
+ this.file = file;
+ this.contentType = contentType;
+ this.originalName = originalName;
+ }
+
+ /**
+ * @return
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * @return
+ */
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * @return
+ */
+ public String getOriginalName() {
+ return originalName;
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/e7a414fe/core/src/main/resources/org/apache/struts2/default.properties
----------------------------------------------------------------------
diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties
index 5d49802..df3e1d4 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -60,6 +60,7 @@ struts.objectFactory.spring.autoWire.alwaysRespect = false
### Parser to handle HTTP POST requests, encoded using the MIME-type multipart/form-data
# struts.multipart.parser=cos
# struts.multipart.parser=pell
+# struts.multipart.parser=jakarta-stream
struts.multipart.parser=jakarta
# uses javax.servlet.context.tempdir by default
struts.multipart.saveDir=
http://git-wip-us.apache.org/repos/asf/struts/blob/e7a414fe/core/src/main/resources/struts-default.xml
----------------------------------------------------------------------
diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml
index 6628c53..b0689a5 100644
--- a/core/src/main/resources/struts-default.xml
+++ b/core/src/main/resources/struts-default.xml
@@ -80,6 +80,7 @@
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="restful2" class="org.apache.struts2.dispatcher.mapper.Restful2ActionMapper" />
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default"/>
+ <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta-stream" class="org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest" scope="default"/>
<bean type="org.apache.struts2.views.TagLibraryDirectiveProvider" name="s" class="org.apache.struts2.views.DefaultTagLibrary" />
<bean type="org.apache.struts2.views.TagLibraryModelProvider" name="s" class="org.apache.struts2.views.DefaultTagLibrary" />