edu.cornell.mannlib.vitro.webapp.controller.MultipartRequestWrapper.java Source code

Java tutorial

Introduction

Here is the source code for edu.cornell.mannlib.vitro.webapp.controller.MultipartRequestWrapper.java

Source

/* $This file is distributed under the terms of the license in /doc/license.txt$ */

package edu.cornell.mannlib.vitro.webapp.controller;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;
import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Wraps a multipart HTTP request, and pre-parses it for file uploads, without
 * losing the request parameters.
 * 
 * Parsing through the request with an Apache ServletFileUpload builds a list of
 * FileItems that includes the parameters and the file parts. After that,
 * however, the parameters are no longer accessible from the request. This
 * wrapper will see that they don't get lost.
 * 
 * The List of FileItems includes both "formField" items and "file" items. As
 * with the usual parameters on a request, we can have more than one value with
 * the same name. So this creates a map of <String, List<String>> to hold the
 * parameters, and a map of <String, List<FileItem>> to hold the files.
 * 
 * The parameters will be available to the wrapper through the normal methods.
 * The files will be available as an attribute that holds the map. Also, a
 * separate attribute will hold a Boolean to indicate that this was indeed a
 * multipart request.
 * 
 * Conveninence methods in VitroRequest will make these easy to handle, without
 * actually touching the attributes.
 */
public class MultipartRequestWrapper extends HttpServletRequestWrapper {
    private static final Log log = LogFactory.getLog(MultipartRequestWrapper.class);

    private static final String CLASS_NAME = MultipartRequestWrapper.class.getSimpleName();
    public static final String ATTRIBUTE_IS_MULTIPART = CLASS_NAME + "_isMultipart";
    public static final String ATTRIBUTE_FILE_ITEM_MAP = CLASS_NAME + "_fileItemMap";
    public static final String ATTRIBUTE_FILE_SIZE_EXCEPTION = CLASS_NAME + "_fileSizeException";

    private static final String[] EMPTY_ARRAY = new String[0];

    // ----------------------------------------------------------------------
    // The factory
    // ----------------------------------------------------------------------

    /**
     * If this is a multipart request, wrap it. Otherwise, just return the
     * request as it is.
     */
    public static HttpServletRequest parse(HttpServletRequest req, ParsingStrategy strategy) throws IOException {
        if (!ServletFileUpload.isMultipartContent(req)) {
            return req;
        }

        ListsMap<String> parameters = new ListsMap<>();
        ListsMap<FileItem> files = new ListsMap<>();

        parseQueryString(req.getQueryString(), parameters);
        parseFileParts(req, parameters, files, strategy);

        return new MultipartRequestWrapper(req, parameters, files);
    }

    /**
     * Pull any parameters out of the URL.
     */
    private static void parseQueryString(String queryString, ListsMap<String> parameters) {
        log.debug("Query string is : '" + queryString + "'");
        if (queryString != null) {
            String[] pieces = queryString.split("&");

            for (String piece : pieces) {
                int equalsHere = piece.indexOf('=');
                if (piece.trim().isEmpty()) {
                    // Ignore an empty piece.
                } else if (equalsHere <= 0) {
                    // A parameter without a value.
                    parameters.add(decode(piece), "");
                } else {
                    // A parameter with a value.
                    String key = piece.substring(0, equalsHere);
                    String value = piece.substring(equalsHere + 1);
                    parameters.add(decode(key), decode(value));
                }
            }
        }
        log.debug("Parameters from query string are: " + parameters);
    }

    /**
     * Remove any special URL-style encoding.
     */
    private static String decode(String encoded) {
        try {
            return URLDecoder.decode(encoded, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error(e, e);
            return encoded;
        }
    }

    private static void parseFileParts(HttpServletRequest req, ListsMap<String> parameters,
            ListsMap<FileItem> files, ParsingStrategy strategy) throws IOException {

        ServletFileUpload upload = createUploadHandler(req, strategy.maximumMultipartFileSize());
        List<FileItem> items = parseRequestIntoFileItems(req, upload, strategy);

        for (FileItem item : items) {
            // Process a regular form field
            String name = item.getFieldName();
            if (item.isFormField()) {
                String value;
                try {
                    value = item.getString("UTF-8");
                } catch (UnsupportedEncodingException e) {
                    value = item.getString();
                }
                parameters.add(name, value);
                log.debug("Form field (parameter) " + name + "=" + value);
            } else {
                files.add(name, item);
                log.debug("File " + name + ": " + item.getSize() + " bytes.");
            }
        }
    }

    /**
     * Create an upload handler that will throw an exception if the file is too
     * large.
     */
    private static ServletFileUpload createUploadHandler(HttpServletRequest req, long maxFileSize) {
        File tempDir = figureTemporaryDirectory(req);

        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD);
        factory.setRepository(tempDir);

        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setSizeMax(maxFileSize);
        return upload;
    }

    /**
     * Find the temporary storage directory for this webapp.
     */
    private static File figureTemporaryDirectory(HttpServletRequest request) {
        return (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
    }

    /**
     * Parse the raw request into a list of parts.
     * 
     * If there is a parsing error, let the strategy handle it. If the strategy
     * throws it back, wrap it in an IOException and throw it on up.
     */
    @SuppressWarnings("unchecked")
    private static List<FileItem> parseRequestIntoFileItems(HttpServletRequest req, ServletFileUpload upload,
            ParsingStrategy strategy) throws IOException {
        try {
            return upload.parseRequest(req);
        } catch (FileSizeLimitExceededException | SizeLimitExceededException e) {
            if (strategy.stashFileSizeException()) {
                req.setAttribute(ATTRIBUTE_FILE_SIZE_EXCEPTION, e);
                return Collections.emptyList();
            } else {
                throw new IOException(e);
            }
        } catch (FileUploadException e) {
            throw new IOException(e);
        }
    }

    // ----------------------------------------------------------------------
    // The instance
    // ----------------------------------------------------------------------

    private final ListsMap<String> parameters;

    public MultipartRequestWrapper(HttpServletRequest req, ListsMap<String> parameters, ListsMap<FileItem> files) {
        super(req);

        this.parameters = parameters;

        req.setAttribute(ATTRIBUTE_IS_MULTIPART, Boolean.TRUE);
        req.setAttribute(ATTRIBUTE_FILE_ITEM_MAP, files);
    }

    /**
     * Look in the map of parsed parameters.
     */
    @Override
    public String getParameter(String name) {
        if (parameters.containsKey(name)) {
            return parameters.get(name).get(0);
        } else {
            return null;
        }
    }

    /**
     * Look in the map of parsed parameters. Make a protective copy.
     */
    @Override
    public Enumeration<?> getParameterNames() {
        return Collections.enumeration(new HashSet<>(parameters.keySet()));
    }

    /**
     * Look in the map of parsed parameters.
     */
    @Override
    public String[] getParameterValues(String name) {
        if (parameters.containsKey(name)) {
            return parameters.get(name).toArray(EMPTY_ARRAY);
        } else {
            return null;
        }
    }

    /**
     * Make a copy of the map of parsed parameters;
     */
    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> result = new HashMap<String, String[]>();
        for (String key : parameters.keySet()) {
            result.put(key, parameters.get(key).toArray(EMPTY_ARRAY));
        }
        return result;
    }

    // ----------------------------------------------------------------------
    // Helper classes
    // ----------------------------------------------------------------------

    private static class ListsMap<T> extends HashMap<String, List<T>> {
        void add(String key, T value) {
            if (!containsKey(key)) {
                put(key, new ArrayList<T>());
            }
            get(key).add(value);
        }
    }

    public interface ParsingStrategy {
        long maximumMultipartFileSize();

        /**
         * Allows you to handle the exception in your code.
         * 
         * Be aware that the multipart parameters have been lost, and that may
         * include form fields.
         */
        boolean stashFileSizeException();
    }

}