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