com.sonicle.webtop.mail.MultipartIterator.java Source code

Java tutorial

Introduction

Here is the source code for com.sonicle.webtop.mail.MultipartIterator.java

Source

/*
* WebTop Services is a web application framework developed by Sonicle S.r.l.
* Copyright (C) 2011 Sonicle S.r.l.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY SONICLE, SONICLE DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Sonicle S.r.l. at email address sonicle@sonicle.com
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Sonicle WebTop" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Sonicle WebTop".
*/

package com.sonicle.webtop.mail;

import java.io.File;
import java.io.IOException;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.vfs2.FileObject;

/**
 * The MultipartIterator class is responsible for reading the
 * input data of a multipart request and splitting it up into
 * input elements, wrapped inside of a
 * {@link org.apache.struts.upload.MultipartElement MultipartElement}
 * for easy definition.  To use this class, create a new instance
 * of MultipartIterator passing it a HttpServletRequest in the
 * constructor.  Then use the {@link #getNextElement() getNextElement}
 * method until it returns null, then you're finished.  Example: <br>
 * <pre>
 *      MultipartIterator iterator = new MultipartIterator(request);
 *      MultipartElement element;
 *
 *      while ((element = iterator.getNextElement()) != null) {
 *           //do something with element
 *      }
 * </pre>
 *
 * @see org.apache.struts.upload.MultipartElement
 * @author Mike Schachter
 */
public class MultipartIterator {

    /**
     * The maximum size in bytes of the buffer used to read lines [4K]
     */
    public static int MAX_LINE_SIZE = 4096;

    /**
     * The request instance for this class
     */
    protected HttpServletRequest request;

    /**
     * The input stream instance for this class
     */
    protected BufferedMultipartInputStream inputStream;

    /**
     * The boundary for this multipart request
     */
    protected String boundary;

    /**
     * The byte array representing the boundary for this multipart request
     */
    protected byte[] boundaryBytes;

    /**
     * Whether or not the input stream is finished
     */
    protected boolean contentRead = false;

    /**
     * The maximum file size in bytes allowed. Ignored if -1
     */
    protected long maxSize = -1;

    /**
     * The total bytes read from this request
     */
    protected long totalLength = 0;

    /**
     * The content length of this request
     */
    protected int contentLength;

    /**
     * The size in bytes written to the filesystem at a time [20K]
     */
    protected int diskBufferSize = 2 * 10240;

    /**
     * The amount of data read from a request at a time.
     * This also represents the maximum size in bytes of
     * a line read from the request [4KB]
     */
    protected int bufferSize = 4096;

    /**
     * The temporary directory to store files
     */
    protected String tempDir;

    /**
     * An output stream to create file
     */
    protected FileObject fileObjectDir = null;

    /**
     * Constructs a MultipartIterator with a default buffer size and no file size
     * limit
     *
     * @param request The multipart request to iterate
     */
    public MultipartIterator(HttpServletRequest request) throws ServletException {
        this(request, -1);
    }

    /**
     * Constructs a MultipartIterator with the specified buffer size and
     * no file size limit
     *
     * @param request The multipart request to iterate
     * @param bufferSize The size in bytes that should be read from the input
     *                   stream at a times
     */
    public MultipartIterator(HttpServletRequest request, int bufferSize) throws ServletException {
        this(request, bufferSize, -1);
    }

    /**
     * Constructs a MultipartIterator with the specified buffer size and
     * the specified file size limit in bytes
     *
     * @param request The multipart request to iterate
     * @param bufferSize The size in bytes that should be read from the input
     *                   stream at a times
     * @param maxSize The maximum size in bytes allowed for a multipart element's data
     */
    public MultipartIterator(HttpServletRequest request, int bufferSize, long maxSize) throws ServletException {

        this(request, bufferSize, maxSize, (String) null);

    }

    public MultipartIterator(HttpServletRequest request, int bufferSize, long maxSize, String tempDir)
            throws ServletException {

        this.request = request;
        this.maxSize = maxSize;
        if (bufferSize > -1) {
            this.bufferSize = bufferSize;
        }
        if (tempDir != null) {
            this.tempDir = tempDir;
        } else {
            //default to system-wide tempdir
            tempDir = System.getProperty("java.io.tmpdir");
        }
        parseRequest();
    }

    public MultipartIterator(HttpServletRequest request, int bufferSize, long maxSize, FileObject dir)
            throws ServletException {

        this.request = request;
        this.maxSize = maxSize;
        if (bufferSize > -1) {
            this.bufferSize = bufferSize;
        }
        this.fileObjectDir = dir;
        parseRequest();
    }

    /**
     * Retrieves the next element in the iterator if one exists.
     *
     * @throws a ServletException if the post size exceeds the maximum file size
     *         passed in the 3 argument constructor
     * @throws an UnsupportedEncodingException if the "ISO-8859-1" encoding isn't found
     * @return a {@link org.apache.struts.upload.MultipartElement MultipartElement}
     *         representing the next element in the request data
     *
     */
    public MultipartElement getNextElement() throws ServletException, UnsupportedEncodingException {
        //retrieve the "Content-Disposition" header
        //and parse
        String disposition = readLine();

        if ((disposition != null) && (disposition.startsWith("Content-Disposition"))) {
            String name = parseDispositionName(disposition);
            String filename = parseDispositionFilename(disposition);

            String contentType = null;
            boolean isFile = (filename != null);

            if (isFile) {
                filename = new File(filename).getName();

                //check for windows filenames,
                //from linux jdk's the entire filepath
                //isn't parsed correctly from File.getName()
                int colonIndex = filename.indexOf(":");
                if (colonIndex == -1) {
                    //check for Window's SMB server file paths
                    colonIndex = filename.indexOf("\\\\");
                }
                int slashIndex = filename.lastIndexOf("\\");

                if ((colonIndex > -1) && (slashIndex > -1)) {
                    //then consider this filename to be a full
                    //windows filepath, and parse it accordingly
                    //to retrieve just the file name
                    filename = filename.substring(slashIndex + 1, filename.length());
                }

                //get the content type
                contentType = readLine();
                contentType = parseContentType(contentType);
            }

            //ignore next line (whitespace) (unless it's a file
            //without content-type)
            if (!((isFile) && contentType == null)) {
                readLine();
            }

            MultipartElement element = null;

            //process a file element
            if (isFile) {
                try {
                    //create a local file on disk representing the element
                    if (fileObjectDir == null) {
                        File elementFile = createLocalFile();
                        element = new MultipartElement(name, filename, contentType, elementFile);
                    } else {
                        FileObject elementFile = createVFSFile(filename);
                        filename = elementFile.getName().getBaseName();
                        element = new MultipartElement(name, filename, contentType, elementFile);
                    }

                } catch (IOException ioe) {
                    Service.logger.error("Exception", ioe);
                    throw new ServletException("IOException while reading file element: " + ioe.getMessage(), ioe);
                }
            } else {
                //read data into String form, then convert to bytes
                //for text
                StringBuffer textData = new StringBuffer();
                String line;
                //parse for text data
                line = readLine();

                while ((line != null) && (!line.startsWith(boundary))) {
                    textData.append(line);
                    line = readLine();
                }

                if (textData.length() > 0) {
                    //cut off "\r" from the end if necessary
                    if (textData.charAt(textData.length() - 1) == '\r') {
                        textData.setLength(textData.length() - 1);
                    }
                }

                //create the element
                element = new MultipartElement(name, textData.toString());
            }
            return element;
        }

        //reset stream
        if (inputStream.markSupported()) {
            try {
                inputStream.reset();
            } catch (IOException ioe) {
                throw new ServletException("IOException while resetting input stream: " + ioe.getMessage());
            }
        }
        return null;
    }

    /**
     * Set the maximum amount of bytes read from a line at one time
     *
     * @see javax.servlet.ServletInputStream#readLine(byte[], int, int)
     */
    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    /**
     * Get the maximum amount of bytes read from a line at one time
     *
     * @see javax.servlet.ServletInputStream#readLine(byte[], int, int)
     */
    public int getBufferSize() {
        return bufferSize;
    }

    /**
     * Set the maximum post data size allowed for a multipart request
     * @param maxSize The maximum post data size in bytes, set to <code>-1</code>
     *                for no limit
     */
    public void setMaxSize(long maxSize) {
        this.maxSize = maxSize;
    }

    /**
     * Get the maximum post data size allowed for a multipart request
     * @return The maximum post data size in bytes
     */
    public long getMaxSize() {
        return maxSize;
    }

    /**
     * Handles retrieving the boundary and setting the input stream
     */
    protected void parseRequest() throws ServletException {

        contentLength = request.getContentLength();

        //set boundary
        boundary = parseBoundary(request.getContentType());
        boundaryBytes = boundary.getBytes();

        try {
            //set the input stream
            inputStream = new BufferedMultipartInputStream(request.getInputStream(), bufferSize, contentLength,
                    maxSize);
            //mark the input stream to allow multiple reads
            if (inputStream.markSupported()) {
                inputStream.mark(contentLength + 1);
            }

        } catch (IOException ioe) {
            throw new ServletException("Problem while reading request: " + ioe.getMessage(), ioe);
        }

        if ((boundary == null) || (boundary.length() < 1)) {
            //try retrieving the header through more "normal" means
            boundary = parseBoundary(request.getHeader("Content-type"));
        }

        if ((boundary == null) || (boundary.length() < 1)) {
            throw new ServletException("MultipartIterator: cannot retrieve boundary " + "for multipart request");
        }

        //read first line
        try {
            String firstLine = readLine();

            if (firstLine == null) {
                throw new ServletException("MultipartIterator: no multipart request data " + "sent");
            }
            if (!firstLine.startsWith(boundary)) {
                throw new ServletException(
                        "MultipartIterator: invalid multipart request " + "data, doesn't start with boundary");
            }
        } catch (UnsupportedEncodingException uee) {
            throw new ServletException("MultipartIterator: encoding \"ISO-8859-1\" not supported");
        }
    }

    /**
     * Parses a content-type String for the boundary.  Appends a
     * "--" to the beginning of the boundary, because thats the
     * real boundary as opposed to the shortened one in the
     * content type.
     */
    public static String parseBoundary(String contentType) {
        if (contentType.lastIndexOf("boundary=") != -1) {
            String _boundary = "--" + contentType.substring(contentType.lastIndexOf("boundary=") + 9);
            if (_boundary.endsWith("\n")) {
                //strip it off
                return _boundary.substring(0, _boundary.length() - 1);
            }
            return _boundary;
        }
        return null;
    }

    /**
     * Parses the "Content-Type" line of a multipart form for a content type
     *
     * @param contentTypeString A String reprsenting the Content-Type line,
     *        with a trailing "\n"
     * @return The content type specified, or <code>null</code> if one can't be
     *         found.
     */
    public static String parseContentType(String contentTypeString) {
        int nameIndex = contentTypeString.indexOf("Content-Type: ");
        if (nameIndex == -1)
            nameIndex = contentTypeString.indexOf("\n");

        if (nameIndex != -1) {
            int endLineIndex = contentTypeString.indexOf("\n");
            if (endLineIndex == -1) {
                endLineIndex = contentTypeString.length() - 1;
            }
            return contentTypeString.substring(nameIndex + 14, endLineIndex);
        }
        return null;
    }

    /**
     * Retrieves the "name" attribute from a content disposition line
     *
     * @param dispositionString The entire "Content-disposition" string
     * @return <code>null</code> if no name could be found, otherwise,
     *         returns the name
     * @see #parseForAttribute(String, String)
     */
    public static String parseDispositionName(String dispositionString) {
        return parseForAttribute("name", dispositionString);
    }

    /**
     * Retrieves the "filename" attribute from a content disposition line
     *
     * @param dispositionString The entire "Content-disposition" string
     * @return <code>null</code> if no filename could be found, otherwise,
     *         returns the filename
     * @see #parseForAttribute(String, String)
     */
    public static String parseDispositionFilename(String dispositionString) {
        return parseForAttribute("filename", dispositionString);
    }

    /**
     * Parses a string looking for a attribute-value pair, and returns the value.
     * For example:
     * <pre>
     *      String parseString = "Content-Disposition: filename=\"bob\" name=\"jack\"";
     *      MultipartIterator.parseForAttribute(parseString, "name");
     * </pre>
     * That will return "bob".
     *
     * @param attribute The name of the attribute you're trying to get
     * @param parseString The string to retrieve the value from
     * @return The value of the attribute, or <code>null</code> if none could be found
     */
    public static String parseForAttribute(String attribute, String parseString) {
        int nameIndex = parseString.indexOf(attribute + "=\"");
        if (nameIndex != -1) {

            int endQuoteIndex = parseString.indexOf("\"", nameIndex + attribute.length() + 3);

            if (endQuoteIndex != -1) {
                return parseString.substring(nameIndex + attribute.length() + 2, endQuoteIndex);
            }
            return "";
        }
        return null;
    }

    /**
     * Reads the input stream until it reaches a new line
     */
    protected String readLine() throws ServletException, UnsupportedEncodingException {

        byte[] bufferByte;
        int bytesRead;

        if (totalLength >= contentLength) {
            return null;
        }

        try {
            bufferByte = inputStream.readLine();
            if (bufferByte == null)
                return null;
            bytesRead = bufferByte.length;
        } catch (IOException ioe) {
            throw new ServletException("IOException while reading multipart request: " + ioe.getMessage());
        }
        if (bytesRead == -1) {
            return null;
        }

        totalLength += bytesRead;
        return new String(bufferByte, 0, bytesRead, "ISO-8859-1");
    }

    /**
     * Creates a file on disk from the current mulitpart element
     * @param fileName the name of the multipart file
     */
    protected File createLocalFile() throws IOException {

        File tempFile = File.createTempFile("strts", null, new File(tempDir));
        BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile), diskBufferSize);
        try {
            writeStream(fos);
        } catch (IOException exc) {
            tempFile.delete();
        }
        return tempFile;
    }

    /**
     * Creates a file on disk from the current mulitpart element
     * @param fileName the name of the multipart file
     */
    protected FileObject createVFSFile(String filename) throws IOException {

        FileObject vfsfile = fileObjectDir.resolveFile(filename);
        int n = 0;
        String xfilename = filename;
        String xext = null;
        int ix = xfilename.lastIndexOf(".");
        if (ix > 0) {
            xext = xfilename.substring(ix + 1);
            xfilename = xfilename.substring(0, ix);
        }
        while (vfsfile.exists()) {
            ++n;
            filename = xfilename + " (" + n + ")";
            if (xext != null)
                filename += "." + xext;
            vfsfile = fileObjectDir.resolveFile(filename);
        }
        OutputStream fos = vfsfile.getContent().getOutputStream();
        try {
            writeStream(fos);
        } catch (IOException exc) {
            vfsfile.delete();
        }
        return vfsfile;
    }

    protected void writeStream(OutputStream fos) throws IOException {
        byte[] lineBuffer = inputStream.readLine();
        if (lineBuffer == null) {
            throw new IOException("Premature end of stream while reading multipart request");
        }
        int bytesRead = lineBuffer.length;

        boolean cutCarriage = false;
        boolean cutNewline = false;

        try {
            while ((bytesRead != -1) && (!equals(lineBuffer, 0, boundaryBytes.length, boundaryBytes))) {

                if (cutCarriage) {
                    fos.write('\r');
                }
                if (cutNewline) {
                    fos.write('\n');
                }
                cutCarriage = false;
                if (bytesRead > 0) {
                    if (lineBuffer[bytesRead - 1] == '\r') {
                        bytesRead--;
                        cutCarriage = true;
                    }
                }
                cutNewline = true;
                fos.write(lineBuffer, 0, bytesRead);
                lineBuffer = inputStream.readLine();
                if (lineBuffer == null) {
                    //throw new IOException("Premature end of stream while reading multipart request");
                    break;
                }
                bytesRead = lineBuffer.length;
            }
        } catch (IOException ioe) {
            fos.close();
            throw ioe;
        }

        fos.flush();
        fos.close();
    }

    /**
     * Checks bytes for equality.  Two byte arrays are equal if
     * each of their elements are the same.  This method checks
     * comp[offset] with source[0] to source[length-1] with
     * comp[offset + length - 1]
     * @param comp The byte to compare to <code>source</code>
     * @param offset The offset to start at in <code>comp</code>
     * @param length The length of <code>comp</code> to compare to
     * @param source The reference byte array to test for equality
     */
    public static boolean equals(byte[] comp, int offset, int length, byte[] source) {

        if ((length != source.length) || (comp.length - offset < length)) {
            return false;
        }

        for (int i = 0; i < length; i++) {
            if (comp[offset + i] != source[i]) {
                return false;
            }
        }
        return true;
    }

}