org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket.protocol.http.servlet;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.apache.commons.io.FileCleaningTracker;
import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.file.FileCleanerTrackerAdapter;
import org.apache.wicket.util.file.IFileCleaner;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Bytes;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.value.ValueMap;

/**
 * Servlet specific WebRequest subclass for multipart content uploads.
 * 
 * @author Jonathan Locke
 * @author Eelco Hillenius
 * @author Cameron Braid
 * @author Ate Douma
 * @author Igor Vaynberg (ivaynberg)
 */
public class MultipartServletWebRequestImpl extends MultipartServletWebRequest {
    /** Map of file items. */
    private final Map<String, List<FileItem>> files;

    /** Map of parameters. */
    private final ValueMap parameters;

    private final String upload;
    private final FileItemFactory fileItemFactory;

    /**
     * total bytes uploaded (downloaded from server's pov) so far. used for upload notifications
     */
    private int bytesUploaded;

    /** content length cache, used for upload notifications */
    private int totalBytes;

    /**
     * Constructor.
     *
     * This constructor will use {@link DiskFileItemFactory} to store uploads.
     *
     * @param request
     *            the servlet request
     * @param filterPrefix
     *            prefix to wicket filter mapping
     * @param maxSize
     *            the maximum size allowed for this request
     * @param upload
     *            upload identifier for {@link UploadInfo}
     * @throws FileUploadException
     *             Thrown if something goes wrong with upload
     */
    public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, Bytes maxSize,
            String upload) throws FileUploadException {
        this(request, filterPrefix, maxSize, upload, new DiskFileItemFactory() {
            @Override
            public FileCleaningTracker getFileCleaningTracker() {
                IFileCleaner fileCleaner = Application.get().getResourceSettings().getFileCleaner();
                return new FileCleanerTrackerAdapter(fileCleaner);
            }
        });
    }

    /**
     * Constructor
     *
     * @param request
     *            the servlet request
     * @param filterPrefix
     *            prefix to wicket filter mapping
     * @param maxSize
     *            the maximum size allowed for this request
     * @param upload
     *            upload identifier for {@link UploadInfo}
     * @param factory
     *            {@link DiskFileItemFactory} to use when creating file items used to represent
     *            uploaded files
     * @throws FileUploadException
     *             Thrown if something goes wrong with upload
     */
    public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, Bytes maxSize,
            String upload, FileItemFactory factory) throws FileUploadException {
        super(request, filterPrefix);

        Args.notNull(upload, "upload");
        this.upload = upload;
        this.fileItemFactory = factory;
        parameters = new ValueMap();
        files = new HashMap<>();

        // Check that request is multipart
        final boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart) {
            throw new IllegalStateException(
                    "ServletRequest does not contain multipart content. One possible solution is to explicitly call Form.setMultipart(true), Wicket tries its best to auto-detect multipart forms but there are certain situation where it cannot.");
        }

        setMaxSize(maxSize);
    }

    @Override
    public void parseFileParts() throws FileUploadException {
        HttpServletRequest request = getContainerRequest();

        // The encoding that will be used to decode the string parameters
        // It should NOT be null at this point, but it may be
        // especially if the older Servlet API 2.2 is used
        String encoding = request.getCharacterEncoding();

        // The encoding can also be null when using multipart/form-data encoded forms.
        // In that case we use the [application-encoding] which we always demand using
        // the attribute 'accept-encoding' in wicket forms.
        if (encoding == null) {
            encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding();
        }

        FileUploadBase fileUpload = newFileUpload(encoding);

        List<FileItem> items;

        if (wantUploadProgressUpdates()) {
            ServletRequestContext ctx = new ServletRequestContext(request) {
                @Override
                public InputStream getInputStream() throws IOException {
                    return new CountingInputStream(super.getInputStream());
                }
            };
            totalBytes = request.getContentLength();

            onUploadStarted(totalBytes);
            try {
                items = fileUpload.parseRequest(ctx);
            } finally {
                onUploadCompleted();
            }
        } else {
            // try to parse the file uploads by using Apache Commons FileUpload APIs
            // because they are feature richer (e.g. progress updates, cleaner)
            items = fileUpload.parseRequest(new ServletRequestContext(request));
            if (items.isEmpty()) {
                // fallback to Servlet 3.0 APIs
                items = readServlet3Parts(request);
            }
        }

        // Loop through items
        for (final FileItem item : items) {
            // Get next item
            // If item is a form field
            if (item.isFormField()) {
                // Set parameter value
                final String value;
                if (encoding != null) {
                    try {
                        value = item.getString(encoding);
                    } catch (UnsupportedEncodingException e) {
                        throw new WicketRuntimeException(e);
                    }
                } else {
                    value = item.getString();
                }

                addParameter(item.getFieldName(), value);
            } else {
                List<FileItem> fileItems = files.get(item.getFieldName());
                if (fileItems == null) {
                    fileItems = new ArrayList<>();
                    files.put(item.getFieldName(), fileItems);
                }
                // Add to file list
                fileItems.add(item);
            }
        }
    }

    /**
     * Reads the uploads' parts by using Servlet 3.0 APIs.
     *
     * <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use
     * upload progress updates.
     *
     * @param request
     *              The http request with the upload data
     * @return A list of {@link FileItem}s
     * @throws FileUploadException
     */
    private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException {
        List<FileItem> itemsFromParts = new ArrayList<>();
        try {
            Collection<Part> parts = request.getParts();
            if (parts != null) {
                for (Part part : parts) {
                    FileItem fileItem = new ServletPartFileItem(part);
                    itemsFromParts.add(fileItem);
                }
            }
        } catch (IOException | ServletException e) {
            throw new FileUploadException("An error occurred while reading the upload parts", e);
        }
        return itemsFromParts;
    }

    /**
     * Factory method for creating new instances of FileUploadBase
     *
     * @param encoding
     *            The encoding to use while reading the data
     * @return A new instance of FileUploadBase
     */
    protected FileUploadBase newFileUpload(String encoding) {
        // Configure the factory here, if desired.
        ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);

        // set encoding specifically when we found it
        if (encoding != null) {
            fileUpload.setHeaderEncoding(encoding);
        }

        fileUpload.setSizeMax(getMaxSize().bytes());

        Bytes fileMaxSize = getFileMaxSize();
        if (fileMaxSize != null) {
            fileUpload.setFileSizeMax(fileMaxSize.bytes());
        }

        return fileUpload;
    }

    /**
    * Adds a parameter to the parameters value map
    * 
    * @param name
    *            parameter name
    * @param value
    *            parameter value
    */
    private void addParameter(final String name, final String value) {
        final String[] currVal = (String[]) parameters.get(name);

        String[] newVal;

        if (currVal != null) {
            newVal = new String[currVal.length + 1];
            System.arraycopy(currVal, 0, newVal, 0, currVal.length);
            newVal[currVal.length] = value;
        } else {
            newVal = new String[] { value };

        }

        parameters.put(name, newVal);
    }

    /**
     * @return Returns the files.
     */
    @Override
    public Map<String, List<FileItem>> getFiles() {
        return files;
    }

    /**
     * Gets the file that was uploaded using the given field name.
     * 
     * @param fieldName
     *            the field name that was used for the upload
     * @return the upload with the given field name
     */
    @Override
    public List<FileItem> getFile(final String fieldName) {
        return files.get(fieldName);
    }

    @Override
    protected Map<String, List<StringValue>> generatePostParameters() {
        Map<String, List<StringValue>> res = new HashMap<>();
        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
            String key = entry.getKey();
            String[] val = (String[]) entry.getValue();
            if (val != null && val.length > 0) {
                List<StringValue> items = new ArrayList<>();
                for (String s : val) {
                    items.add(StringValue.valueOf(s));
                }
                res.put(key, items);
            }
        }
        return res;
    }

    /**
     * Subclasses that want to receive upload notifications should return true. By default it takes
     * the value from {@link org.apache.wicket.settings.ApplicationSettings#isUploadProgressUpdatesEnabled()}.
     * 
     * @return true if upload status update event should be invoked
     */
    protected boolean wantUploadProgressUpdates() {
        return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled();
    }

    /**
     * Upload start callback
     * 
     * @param totalBytes
     */
    protected void onUploadStarted(int totalBytes) {
        UploadInfo info = new UploadInfo(totalBytes);

        setUploadInfo(getContainerRequest(), upload, info);
    }

    /**
     * Upload status update callback
     * 
     * @param bytesUploaded
     * @param total
     */
    protected void onUploadUpdate(int bytesUploaded, int total) {
        HttpServletRequest request = getContainerRequest();
        UploadInfo info = getUploadInfo(request, upload);
        if (info == null) {
            throw new IllegalStateException(
                    "could not find UploadInfo object in session which should have been set when uploaded started");
        }
        info.setBytesUploaded(bytesUploaded);

        setUploadInfo(request, upload, info);
    }

    /**
     * Upload completed callback
     */
    protected void onUploadCompleted() {
        clearUploadInfo(getContainerRequest(), upload);
    }

    /**
     * An {@link InputStream} that updates total number of bytes read
     * 
     * @author Igor Vaynberg (ivaynberg)
     */
    private class CountingInputStream extends InputStream {

        private final InputStream in;

        /**
         * Constructs a new CountingInputStream.
         * 
         * @param in
         *            InputStream to delegate to
         */
        public CountingInputStream(InputStream in) {
            this.in = in;
        }

        /**
         * @see java.io.InputStream#read()
         */
        @Override
        public int read() throws IOException {
            int read = in.read();
            bytesUploaded += (read < 0) ? 0 : 1;
            onUploadUpdate(bytesUploaded, totalBytes);
            return read;
        }

        /**
         * @see java.io.InputStream#read(byte[])
         */
        @Override
        public int read(byte[] b) throws IOException {
            int read = in.read(b);
            bytesUploaded += (read < 0) ? 0 : read;
            onUploadUpdate(bytesUploaded, totalBytes);
            return read;
        }

        /**
         * @see java.io.InputStream#read(byte[], int, int)
         */
        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            int read = in.read(b, off, len);
            bytesUploaded += (read < 0) ? 0 : read;
            onUploadUpdate(bytesUploaded, totalBytes);
            return read;
        }

    }

    @Override
    public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload)
            throws FileUploadException {
        // FIXME mgrigorov: Why these checks are made here ?!
        // Why they are not done also at org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.newMultipartWebRequest(org.apache.wicket.util.lang.Bytes, java.lang.String, org.apache.wicket.util.upload.FileItemFactory)() ?
        // Why there is no check that the summary of all files' sizes is less than the set maxSize ?
        // Setting a breakpoint here never breaks with the standard upload examples.

        Bytes fileMaxSize = getFileMaxSize();
        for (Map.Entry<String, List<FileItem>> entry : files.entrySet()) {
            List<FileItem> fileItems = entry.getValue();
            for (FileItem fileItem : fileItems) {
                if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes()) {
                    String fieldName = entry.getKey();
                    FileUploadException fslex = new FileUploadBase.FileSizeLimitExceededException("The field '"
                            + fieldName + "' exceeds its maximum permitted size of '" + maxSize + "' characters.",
                            fileItem.getSize(), fileMaxSize.bytes());
                    throw fslex;
                }
            }
        }
        return this;
    }

    @Override
    public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory)
            throws FileUploadException {
        return this;
    }

    private static final String SESSION_KEY = MultipartServletWebRequestImpl.class.getName();

    private static String getSessionKey(String upload) {
        return SESSION_KEY + ":" + upload;
    }

    /**
     * Retrieves {@link UploadInfo} from session, null if not found.
     * 
     * @param req
     *            http servlet request, not null
     * @param upload
     *            upload identifier
     * @return {@link UploadInfo} object from session, or null if not found
     */
    public static UploadInfo getUploadInfo(final HttpServletRequest req, String upload) {
        Args.notNull(req, "req");
        return (UploadInfo) req.getSession().getAttribute(getSessionKey(upload));
    }

    /**
     * Sets the {@link UploadInfo} object into session.
     * 
     * @param req
     *            http servlet request, not null
     * @param upload
     *            upload identifier
     * @param uploadInfo
     *            {@link UploadInfo} object to be put into session, not null
     */
    public static void setUploadInfo(final HttpServletRequest req, String upload, final UploadInfo uploadInfo) {
        Args.notNull(req, "req");
        Args.notNull(upload, "upload");
        Args.notNull(uploadInfo, "uploadInfo");
        req.getSession().setAttribute(getSessionKey(upload), uploadInfo);
    }

    /**
     * Clears the {@link UploadInfo} object from session if one exists.
     * 
     * @param req
     *            http servlet request, not null
     * @param upload
     *            upload identifier
     */
    public static void clearUploadInfo(final HttpServletRequest req, String upload) {
        Args.notNull(req, "req");
        Args.notNull(upload, "upload");
        req.getSession().removeAttribute(getSessionKey(upload));
    }

}