diff options
Diffstat (limited to 'src/com/itmill/toolkit/terminal/gwt/server/MultipartRequest.java')
-rw-r--r-- | src/com/itmill/toolkit/terminal/gwt/server/MultipartRequest.java | 1332 |
1 files changed, 1332 insertions, 0 deletions
diff --git a/src/com/itmill/toolkit/terminal/gwt/server/MultipartRequest.java b/src/com/itmill/toolkit/terminal/gwt/server/MultipartRequest.java new file mode 100644 index 0000000000..ae22f5bbbb --- /dev/null +++ b/src/com/itmill/toolkit/terminal/gwt/server/MultipartRequest.java @@ -0,0 +1,1332 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Interfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license.pdf. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.terminal.gwt.server; + +import java.util.Hashtable; +import java.io.BufferedOutputStream; +import java.io.BufferedInputStream; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; +import java.io.File; + +/** + * A Multipart form data parser. Parses an input stream and writes out any files + * found, making available a hashtable of other url parameters. As of version + * 1.17 the files can be saved to memory, and optionally written to a database, + * etc. + * + * <BR> + * <BR> + * Copyright (C)2001 Jason Pell. <BR> + * + * <PRE> + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. <BR> + * This library 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 Lesser General Public License for more + * details. <BR> + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA <BR> + * Email: jasonpell@hotmail.com Url: http://www.geocities.com/jasonpell + * + * </PRE> + * + * @author Jason Pell + * + * @version 1.18 Fixed some serious bugs. A new method readAndWrite(InputStream + * in, OutputStream out) which now does the generic processing in + * common for readAndWriteFile and readFile. The differences are that + * now the two extra bytes at the end of a file upload are processed + * once, instead of after each line. Also if an empty file is + * encountered, an outputstream is opened, but then deleted if no data + * written to it. The <code>getCharArray</code> method has been + * removed. Replaced by the new String(bytes, encoding) method using a + * specific encoding (Defaults to ISO-8859-1) to ensure that extended + * characters are supported. All strings are processed using this + * encoding. The addition of static methods setEncoding(String) and + * <code>getEncoding</code> method to allow the use of + * <code>MultipartRequest</code> with a specific encoding type. All + * instances of MultipartRequest will utilise the static charEncoding + * variable value, that the <code>setEncoding</code> method can be + * used to set. Started to introduce support for multiple file uploads + * with the same form field name, but not completed for v1.18. + * 26/06/2001 + * + * @version 1.17 A few _very_ minor fixes. Plus a cool new feature added. The + * ability to save files into memory. <b>Thanks to Mark Latham for the + * idea and some of the code.</b> 11/04/2001 + * @version 1.16 Added support for multiple parameter values. Also fixed + * getCharArray(...) method to support parameters with non-english + * ascii values (ascii above 127). Thanks to Stefan Schmidt & Michael + * Elvers for this. (No fix yet for reported problems with Tomcat 3.2 + * or a single extra byte appended to uploads of certain files). By + * 1.17 hopefully will have a resolution for the second problem. + * 14/03/2001 + * @version 1.15 A new parameter added, intMaxReadBytes, to allow arbitrary + * length files. Released under the LGPL (Lesser General Public + * License). 03/02/2001 + * @version 1.14 Fix for IE problem with filename being empty. This is because + * IE includes a default Content-Type even when no file is uploaded. + * 16/02/2001 + * @version 1.13 If an upload directory is not specified, then all file contents + * are sent into oblivion, but the rest of the parsing works as normal. + * @version 1.12 Fix, was allowing zero length files. Will not even create the + * output file until there is something to write. getFile(String) now + * returns null, if a zero length file was specified. 06/11/2000 + * @version 1.11 Fix, in case Content-type is not specified. + * @version 1.1 Removed dependence on Servlets. Now passes in a generic + * InputStream instead. "Borrowed" readLine from Tomcat 3.1 + * ServletInputStream class, so we can remove some of the dependencies + * on ServletInputStream. Fixed bug where a empty INPUT TYPE="FILE" + * value, would cause an exception. + * @version 1.0 Initial Release. + */ + +public class MultipartRequest { + /** + * Defines Character Encoding method here. + */ + private String charEncoding = "UTF-8"; + + // If not null, send debugging out here. + private PrintWriter debug = null; + + private Hashtable htParameters = null; + + private Hashtable htFiles = null; + + private String strBoundary = null; + + // If this Directory spec remains null, writing of files will be disabled... + private File fileOutPutDirectory = null; + + private boolean loadIntoMemory = false; + + private long intContentLength = -1; + + private long intTotalRead = -1; + + /** + * Prevent a denial of service by defining this, will never read more data. + * If Content-Length is specified to be more than this, will throw an + * exception. + * + * This limits the maximum number of bytes to the value of an int, which is + * 2 Gigabytes. + */ + public static final int MAX_READ_BYTES = Integer.MAX_VALUE; + + /** + * Defines the number of bytes to read per readLine call. 128K + */ + public static final int READ_LINE_BLOCK = 1024 * 128; + + /** + * Store a read from the input stream here. Global so we do not keep + * creating new arrays each read. + */ + private byte[] blockOfBytes = null; + + /** + * Type constant for File FILENAME. + */ + public static final int FILENAME = 0; + + /** + * Type constant for the File CONTENT_TYPE. + */ + public static final int CONTENT_TYPE = 1; + + /** + * Type constant for the File SIZE. + */ + public static final int SIZE = 2; + + /** + * Type constant for the File CONTENTS. + * + * <b>Note: </b>Only used for file upload to memory. + */ + public static final int CONTENTS = 3; + + /** + * This method should be called on the <code>MultipartRequest</code> + * itself, not on any instances of <code>MultipartRequest</code>, because + * this sets up the encoding for all instances of multipartrequest. You can + * set the encoding to null, in which case the default encoding will be + * applied. The default encoding if this method is not called has been set + * to ISO-8859-1, which seems to offer the best hope of support for + * international characters, such as german "Umlaut" characters. + * + * <p> + * <b>Warning:</b> In multithreaded environments it is the responsibility + * of the implementer to make sure that this method is not called while + * another instance is being constructed. When an instance of + * MultipartRequest is constructed, it parses the input data, and uses the + * result of <code>getEncoding</code> method to convert between bytes and + * strings. If <code>setEncoding</code> method is called by another + * thread, while the private <code>parse</code> method is executing, the + * method will utilise this new encoding, which may cause serious problems. + * </p> + */ + public void setEncoding(String enc) throws UnsupportedEncodingException { + if (enc == null || enc.trim() == "") + charEncoding = System.getProperty("file.encoding"); + else { + // This will test the encoding for validity. + new String(new byte[] { '\n' }, enc); + + charEncoding = enc; + } + } + + /** + * Gets the current encoding method. + * + * @return the encoding method. + */ + public String getEncoding() { + return charEncoding; + } + + /** + * Constructor. + * + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param strSaveDirectory + * the temporary directory to save the file from where they can + * then be moved to wherever by the calling process. <b>If you + * specify <u>null</u> for this parameter, then any files + * uploaded will be silently ignored.</b> + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + */ + public MultipartRequest(String strContentTypeText, int intContentLength, + InputStream in, String strSaveDirectory) + throws IllegalArgumentException, IOException { + this(null, strContentTypeText, intContentLength, in, strSaveDirectory, + MAX_READ_BYTES); + } + + /** + * Constructor. + * + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param strSaveDirectory + * the temporary directory to save the file from where they can + * then be moved to wherever by the calling process. <b>If you + * specify <u>null</u> for this parameter, then any files + * uploaded will be silently ignored.</B> + * @param intMaxReadBytes + * Overrides the MAX_BYTES_READ value, to allow arbitrarily long + * files. + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + */ + public MultipartRequest(String strContentTypeText, int intContentLength, + InputStream in, String strSaveDirectory, int intMaxReadBytes) + throws IllegalArgumentException, IOException { + this(null, strContentTypeText, intContentLength, in, strSaveDirectory, + intMaxReadBytes); + } + + /** + * Constructor. + * + * @param debug + * A PrintWriter that can be used for debugging. + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param strSaveDirectory + * the temporary directory to save the file from where they can + * then be moved to wherever by the calling process. <b>If you + * specify <u>null</u> for this parameter, then any files + * uploaded will be silently ignored.</B> + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + * @deprecated Replaced by MultipartRequest(PrintWriter, String, int, + * InputStream, int) You can specify + * MultipartRequest.MAX_READ_BYTES for the intMaxReadBytes + * parameter + */ + public MultipartRequest(PrintWriter debug, String strContentTypeText, + int intContentLength, InputStream in, String strSaveDirectory) + throws IllegalArgumentException, IOException { + this(debug, strContentTypeText, intContentLength, in, strSaveDirectory, + MAX_READ_BYTES); + + } + + /** + * Constructor - load into memory constructor + * + * @param debug + * A PrintWriter that can be used for debugging. + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param intMaxReadBytes + * Overrides the MAX_BYTES_READ value, to allow arbitrarily long + * files. + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + */ + public MultipartRequest(PrintWriter debug, String strContentTypeText, + int intContentLength, InputStream in, int intMaxReadBytes) + throws IllegalArgumentException, IOException { + this.loadIntoMemory = true; + + // Now initialise the object, which will actually call the parse method + // to parse multipart stream. + init(debug, strContentTypeText, intContentLength, in, intMaxReadBytes); + } + + /** + * Constructor. + * + * @param debug + * A PrintWriter that can be used for debugging. + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param strSaveDirectory + * the temporary directory to save the file from where they can + * then be moved to wherever by the calling process. <b>If you + * specify <u>null</u> for this parameter, then any files + * uploaded will be silently ignored.</B> + * @param intMaxReadBytes + * Overrides the MAX_BYTES_READ value, to allow arbitrarily long + * files. + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + */ + public MultipartRequest(PrintWriter debug, String strContentTypeText, + int intContentLength, InputStream in, String strSaveDirectory, + int intMaxReadBytes) throws IllegalArgumentException, IOException { + // IF strSaveDirectory == NULL, then we should ignore any files + // uploaded. + if (strSaveDirectory != null) { + fileOutPutDirectory = new File(strSaveDirectory); + if (!fileOutPutDirectory.exists()) + throw new IOException("Directory [" + strSaveDirectory + + "] is invalid."); + else if (!fileOutPutDirectory.canWrite()) + throw new IOException("Directory [" + strSaveDirectory + + "] is readonly."); + } + + // Now initialise the object, which will actually call the parse method + // to parse multipart stream. + init(debug, strContentTypeText, intContentLength, in, intMaxReadBytes); + } + + /** + * Initialise the parser. + * + * @param debug + * A PrintWriter that can be used for debugging. + * @param strContentTypeText + * the "Content-Type" HTTP header value. + * @param intContentLength + * the "Content-Length" HTTP header value. + * @param in + * the InputStream to read and parse. + * @param strSaveDirectory + * the temporary directory to save the file from where they can + * then be moved to wherever by the calling process. <b>If you + * specify <u>null</u> for this parameter, then any files + * uploaded will be silently ignored.</B> + * @param intMaxReadBytes + * Overrides the MAX_BYTES_READ value, to allow arbitrarily long + * files. + * + * @exception IllegalArgumentException + * If the strContentTypeText does not contain a Content-Type + * of "multipart/form-data" or the boundary is not found. + * @exception IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + * + * @see #MAX_READ_BYTES + */ + private void init(PrintWriter debug, String strContentTypeText, + int intContentLength, InputStream in, int intMaxReadBytes) + throws IllegalArgumentException, IOException { + // saves reference to debug stream for later. + this.debug = debug; + + if (strContentTypeText != null + && strContentTypeText.startsWith("multipart/form-data") + && strContentTypeText.indexOf("boundary=") != -1) + strBoundary = strContentTypeText.substring( + strContentTypeText.indexOf("boundary=") + + "boundary=".length()).trim(); + else { + // <mtl,jpell> + debug("ContentType = " + strContentTypeText); + throw new IllegalArgumentException("Invalid Content Type."); + } + + this.intContentLength = intContentLength; + // FIX: 115 + if (intContentLength > intMaxReadBytes) + throw new IOException("Content Length Error (" + intContentLength + + " > " + intMaxReadBytes + ")"); + + // Instantiate the hashtable... + htParameters = new Hashtable(); + htFiles = new Hashtable(); + blockOfBytes = new byte[READ_LINE_BLOCK]; + + // Now parse the data. + parse(new BufferedInputStream(in)); + + // No need for this once parse is complete. + this.blockOfBytes = null; + this.debug = null; + this.strBoundary = null; + } + + /** + * Gets the value of the strName URLParameter. If more than one value for a + * particular Parameter, will return the first. If an error occurs will + * return null. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return the value of the URL Parameter. + */ + public String getURLParameter(String strName) { + Object value = htParameters.get(strName); + if (value instanceof Vector) + return (String) ((Vector) value).firstElement(); + else + return (String) htParameters.get(strName); + } + + /** + * Gets an enumeration of all values for the strName parameter. Even if a + * single value for, will always return an enumeration, although it may + * actually be empty if no value was encountered for strName or it is an + * invalid parameter name. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return the enumeration of all values. + */ + public Enumeration getURLParameters(String strName) { + Object value = htParameters.get(strName); + if (value instanceof Vector) + return ((Vector) value).elements(); + else { + Vector vector = new Vector(); + if (value != null) + vector.addElement(value); + return vector.elements(); + } + } + + /** + * Gets the enumeration of all URL Parameters for the current HTTP Request. + * + * @return the enumeration of URl Parameters. + */ + public Enumeration getParameterNames() { + return htParameters.keys(); + } + + /** + * Gets the enumeration of all INPUT TYPE=FILE parameter NAMES as + * encountered during the upload. + * + * @return + */ + public Enumeration getFileParameterNames() { + return htFiles.keys(); + } + + /** + * Returns the Content-Type of a file. + * + * @see #getFileParameter(java.lang.String, int) + */ + public String getContentType(String strName) { + // Can cast null, it will be ignored. + return (String) getFileParameter(strName, CONTENT_TYPE); + } + + /** + * If files were uploaded into memory, this method will retrieve the + * contents of the file as a InputStream. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return the contents of the file as a InputStream, or null if not file + * uploaded, or file uploaded to file system directory. + * @see #getFileParameter(java.lang.String, int) + */ + public InputStream getFileContents(String strName) { + Object obj = getFileParameter(strName, CONTENTS); + if (obj != null) + return new ByteArrayInputStream((byte[]) obj); + else + return null; + } + + /** + * Returns a File reference to the uploaded file. This reference is to the + * files uploaded location, and allows you to read/move/delete the file. + * This method is only of use, if files were uploaded to the file system. + * Will return null if uploaded to memory, in which case you should use + * getFileContents(strName) instead. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return a null file reference if a call to getFileSize(strName) returns + * zero or files were uploaded to memory. + * @see #getFileSize(java.lang.String) + * @see #getFileContents(java.lang.String) + * @see #getFileSystemName(java.lang.String) + */ + public File getFile(String strName) { + String filename = getFileSystemName(strName); + // Fix: If fileOutPutDirectory is null, then we are ignoring any file + // contents, so we must return null. + if (filename != null && getFileSize(strName) > 0 + && fileOutPutDirectory != null) + return new File(fileOutPutDirectory, filename); + else + return null; + } + + /** + * Gets the file system basename of an uploaded file. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return null if strName not found. + * + * @see #getFileParameter(java.lang.String, int) + */ + public String getFileSystemName(String strName) { + // Can cast null, it will be ignored. + return (String) getFileParameter(strName, FILENAME); + } + + /** + * Returns the File Size of a uploaded file. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @return -1 if file size not defined. + * + * @see #getFileParameter(java.lang.String, int) + */ + public long getFileSize(String strName) { + Object obj = getFileParameter(strName, SIZE); + if (obj != null) + return ((Long) obj).longValue(); + else + return (long) -1; + } + + /** + * Access an attribute of a file upload parameter record. + * <p> + * The getFileSystemName(String strName),getFileSize(String + * strName),getContentType(String strName), getContents(String strName) + * methods all use this method passing in a different type argument. + * </p> + * + * <p> + * <b>Note: </b>This class has been changed to provide for future + * functionality where you will be able to access all files uploaded, even + * if they are uploaded using the same form field name. At this point + * however, only the first file uploaded via a form field name is + * accessible. + * </p> + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * + * @param type + * What attribute you want from the File Parameter. The following + * types are supported: MultipartRequest.FILENAME, + * MultipartRequest.CONTENT_TYPE, MultipartRequest.SIZE, + * MultipartRequest.CONTENTS + * + * @see #getContentType(java.lang.String) + * @see #getFileSize(java.lang.String) + * @see #getFileContents(java.lang.String) + * @see #getFileSystemName(java.lang.String) + */ + public Object getFileParameter(String strName, int type) { + Object[] objArray = null; + Object value = htFiles.get(strName); + if (value instanceof Vector) + objArray = (Object[]) ((Vector) value).firstElement(); + else + objArray = (Object[]) htFiles.get(strName); + + // Now ensure valid value. + if (objArray != null && type >= FILENAME && type <= CONTENTS) + return objArray[type]; + else + return null; + } + + /** + * This is the main parse method. + * + * @param in + * the InputStream to read and parse. + * @throws IOException + * If the intContentLength is higher than MAX_READ_BYTES or + * strSaveDirectory is invalid or cannot be written to. + */ + private void parse(InputStream in) throws IOException { + String strContentType = null; + String strName = null; + String strFilename = null; + String strLine = null; + int read = -1; + + // First run through, check that the first line is a boundary, otherwise + // throw a exception as format incorrect. + read = readLine(in, blockOfBytes); + strLine = read > 0 ? new String(blockOfBytes, 0, read, charEncoding) + : null; + + // Must be boundary at top of loop, otherwise we have finished. + if (strLine == null || strLine.indexOf(this.strBoundary) == -1) + // Just exit. Exception would be overkill + return; + // throw new IOException("Invalid Form Data, no boundary encountered."); + + // At the top of loop, we assume that the Content-Disposition line is + // next, otherwise we are at the end. + while (true) { + // Get Content-Disposition line. + read = readLine(in, blockOfBytes); + if (read <= 0) + break; // Nothing to do. + else { + strLine = new String(blockOfBytes, 0, read, charEncoding); + + // Mac IE4 adds extra line after last boundary - 1.21 + if (strLine == null || strLine.length() == 0 + || strLine.trim().length() == 0) + break; + + strName = trimQuotes(getValue("name", strLine)); + // If this is not null, it indicates that we are processing a + // filename. + strFilename = trimQuotes(getValue("filename", strLine)); + // Now if not null, strip it of any directory information. + + if (strFilename != null) { + // Fix: did not check whether filename was empty string + // indicating FILE contents were not passed. + if (strFilename.length() > 0) { + // Need to get the content type. + read = readLine(in, blockOfBytes); + strLine = read > 0 ? new String(blockOfBytes, 0, read, + charEncoding) : null; + + strContentType = "application/octet-stream"; + // Fix 1.11: If not null AND strLine.length() is long + // enough. + if (strLine != null + && strLine.length() > "Content-Type: ".length()) + strContentType = strLine.substring("Content-Type: " + .length());// Changed 1.13 + } else { + // FIX 1.14: IE problem with empty filename. + read = readLine(in, blockOfBytes); + strLine = read > 0 ? new String(blockOfBytes, 0, read, + charEncoding) : null; + + if (strLine != null + && strLine.startsWith("Content-Type:")) + readLine(in, blockOfBytes); + } + } + + // Ignore next line, as it should be blank. + readLine(in, blockOfBytes); + + // No filename specified at all. + if (strFilename == null) { + String param = readParameter(in); + addParameter(strName, param); + } else { + if (strFilename.length() > 0) { + long filesize = -1; + // Will remain null for read onto file system uploads. + byte[] contentsOfFile = null; + + // Get the BASENAME version of strFilename. + strFilename = getBasename(strFilename); + + // Are we loading files into memory instead of the + // filesystem? + if (loadIntoMemory) { + contentsOfFile = readFile(in); + if (contentsOfFile != null) + filesize = contentsOfFile.length; + } else + // Read the file onto file system. + filesize = readAndWriteFile(in, strFilename); + + // Fix 1.18 for multiple FILE parameter values. + if (filesize > 0) + addFileParameter(strName, new Object[] { + strFilename, strContentType, + new Long(filesize), contentsOfFile }); + else + // Zero length file. + addFileParameter(strName, new Object[] { + strFilename, null, new Long(0), null }); + } else // Fix: FILE INPUT TYPE, but no file passed as + // input... + { + addFileParameter(strName, new Object[] { null, null, + null, null }); + readLine(in, blockOfBytes); + } + } + } + }// while + } + + /** + * So we can put the logic for supporting multiple parameters with the same + * form field name in the one location. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @param value + * the form field value. + */ + private void addParameter(String strName, String value) { + // Fix NPE in case of null name + if (strName == null) + return; + + // Fix 1.16: for multiple parameter values. + Object objParms = htParameters.get(strName); + + // Adds an new entry to the param vector. + if (objParms instanceof Vector) + ((Vector) objParms).addElement(value); + else if (objParms instanceof String)// There is only one entry, so we + // create a vector! + { + Vector vecParms = new Vector(); + vecParms.addElement(objParms); + vecParms.addElement(value); + + htParameters.put(strName, vecParms); + } else + // first entry for strName. + htParameters.put(strName, value); + } + + /** + * So we can put the logic for supporting multiple files with the same form + * field name in the one location. + * + * Assumes that this method will never be called with a null fileObj or + * strFilename. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @param fileObj + */ + private void addFileParameter(String strName, Object[] fileObj) { + Object objParms = htFiles.get(strName); + + // Add an new entry to the param vector. + if (objParms instanceof Vector) + ((Vector) objParms).addElement(fileObj); + else if (objParms instanceof Object[])// There is only one entry, so + // we create a vector! + { + Vector vecParms = new Vector(); + vecParms.addElement(objParms); + vecParms.addElement(fileObj); + + htFiles.put(strName, vecParms); + } else + // first entry for strName. + htFiles.put(strName, fileObj); + } + + /** + * Reads the parameters, assume already passed Content-Disposition and blank + * line. + * + * @param in + * the InputStream to read and parse. + * @return the value read in. + * @throws IOException + * if an error occurs writing the file. + */ + private String readParameter(InputStream in) throws IOException { + StringBuffer buf = new StringBuffer(); + int read = -1; + + String line = null; + while (true) { + read = readLine(in, blockOfBytes); + if (read < 0) + throw new IOException("Stream ended prematurely."); + + // Change v1.18: Only instantiate string once for performance + // reasons. + line = new String(blockOfBytes, 0, read, charEncoding); + if (read < blockOfBytes.length + && line.indexOf(this.strBoundary) != -1) + break; // Boundary found, we need to finish up. + else + buf.append(line); + } + + if (buf.length() > 0) + buf.setLength(getLengthMinusEnding(buf)); + return buf.toString(); + } + + /** + * Read from in, write to out, minus last two line ending bytes. + * + * @param in + * the InputStream to read and parse. + * @param out + * the OutputStream. + * @throws IOException + * if an error occurs writing the file. + */ + private long readAndWrite(InputStream in, OutputStream out) + throws IOException { + long fileSize = 0; + int read = -1; + + // This variable will be assigned the bytes actually read. + byte[] secondLineOfBytes = new byte[blockOfBytes.length]; + // So we do not have to keep creating the second array. + int sizeOfSecondArray = 0; + + while (true) { + read = readLine(in, blockOfBytes); + if (read < 0) + throw new IOException("Stream ended prematurely."); + + // Found boundary. + if (read < blockOfBytes.length + && new String(blockOfBytes, 0, read, charEncoding) + .indexOf(this.strBoundary) != -1) { + // Writes the line, minus any line ending bytes. + // The secondLineOfBytes will NEVER BE NON-NULL if out==null, so + // there is no need to included this in the test + if (sizeOfSecondArray != 0) { + // Only used once, so declare here. + int actualLength = getLengthMinusEnding(secondLineOfBytes, + sizeOfSecondArray); + if (actualLength > 0 && out != null) { + out.write(secondLineOfBytes, 0, actualLength); + // Updates the file size. + fileSize += actualLength; + } + } + break; + } else { + // Writes the out previous line. + // The sizeOfSecondArray will NEVER BE ZERO if out==null, so + // there is no need to included this in the test + if (sizeOfSecondArray != 0) { + out.write(secondLineOfBytes, 0, sizeOfSecondArray); + // Updates the file size. + fileSize += sizeOfSecondArray; + } + + // out will always be null, so there is no need to reset + // sizeOfSecondArray to zero each time. + if (out != null) { + // Copy the read bytes into the array. + System.arraycopy(blockOfBytes, 0, secondLineOfBytes, 0, + read); + // That is how many bytes to read from the secondLineOfBytes + sizeOfSecondArray = read; + } + } + } + + // Returns the number of bytes written to outstream. + return fileSize; + } + + /** + * Reads a Multipart section that is a file type. Assumes that the + * Content-Disposition/Content-Type and blank line have already been + * processed. So we read until we hit a boundary, then close file and + * return. + * + * @param in + * the InputStream to read and parse. + * @param strFilename + * the FileSystemName of the file. + * @return the number of bytes read. + * @throws IOException + * if an error occurs writing the file. + */ + private long readAndWriteFile(InputStream in, String strFilename) + throws IOException { + // Stores a reference to this, as we may need to delete it later. + File outFile = new File(fileOutPutDirectory, strFilename); + + BufferedOutputStream out = null; + // Do not bother opening a OutputStream, if we cannot even write the + // file. + if (fileOutPutDirectory != null) + out = new BufferedOutputStream(new FileOutputStream(outFile)); + + long count = readAndWrite(in, out); + // Count would NOT be larger than zero if out was null. + if (count > 0) { + out.flush(); + out.close(); + } else { + out.close(); + // Deletes the file as empty. We should be able to delete it, if we + // can open it! + outFile.delete(); + } + return count; + } + + /** + * If the fileOutPutDirectory wasn't specified, just read the file to + * memory. + * + * @param in + * the InputStream to read and parse. + * @return contents of file, from which you can garner the size as well. + * @throws IOException + * if the writing failed due to input/output error. + */ + private byte[] readFile(InputStream in) throws IOException { + // In this case, we do not need to worry about a outputdirectory. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + long count = readAndWrite(in, out); + // Count would NOT be larger than zero if out was null. + if (count > 0) { + // Return contents of file to parse method for inclusion in htFiles + // object. + return out.toByteArray(); + } else + return null; + } + + /** + * Gets the length of the line minus line ending. + * + * @param byteLine + * @param endOfArray + * This is because in many cases the byteLine will have garbage + * data at the end, so we act as though the actual end of the + * array is this parameter. If you want to process the complete + * byteLine, specify byteLine.length as the endOfArray parameter. + * @return the length. + */ + private static final int getLengthMinusEnding(byte byteLine[], + int endOfArray) { + if (byteLine == null) + return 0; + + if (endOfArray >= 2 && byteLine[endOfArray - 2] == '\r' + && byteLine[endOfArray - 1] == '\n') + return endOfArray - 2; + else if (endOfArray >= 1 && byteLine[endOfArray - 1] == '\n' + || byteLine[endOfArray - 1] == '\r') + return endOfArray - 1; + else + return endOfArray; + } + + /** + * + * @param buf + * @return + */ + private static final int getLengthMinusEnding(StringBuffer buf) { + if (buf.length() >= 2 && buf.charAt(buf.length() - 2) == '\r' + && buf.charAt(buf.length() - 1) == '\n') + return buf.length() - 2; + else if (buf.length() >= 1 && buf.charAt(buf.length() - 1) == '\n' + || buf.charAt(buf.length() - 1) == '\r') + return buf.length() - 1; + else + return buf.length(); + } + + /** + * Reads at most READ_BLOCK blocks of data, or a single line whichever is + * smaller. Returns -1, if nothing to read, or we have reached the specified + * content-length. + * + * Assumes that bytToBeRead.length indicates the block size to read. + * + * @param in + * the InputStream to read and parse. + * @param bytesToBeRead + * the bytes to be read. + * @return -1 if stream has ended, before a newline encountered (should + * never happen) OR we have read past the Content-Length specified. + * (Should also not happen). Otherwise return the number of + * characters read. You can test whether the number returned is less + * than bytesToBeRead.length, which indicates that we have read the + * last line of a file or parameter or a border line, or some other + * formatting stuff. + * @throws IOException + * if the writing failed due to input/output error. + */ + private int readLine(InputStream in, byte[] bytesToBeRead) + throws IOException { + // Ensure that there is still stuff to read... + if (intTotalRead >= intContentLength) + return -1; + + // Get the length of what we are wanting to read. + int length = bytesToBeRead.length; + + // End of content, but some servers (apparently) may not realise this + // and end the InputStream, so + // we cover ourselves this way. + if (length > (intContentLength - intTotalRead)) + length = (int) (intContentLength - intTotalRead); // So we only + // read the data + // that is left. + + int result = readLine(in, bytesToBeRead, 0, length); + // Only if we get actually read something, otherwise something weird has + // happened, such as the end of stream. + if (result > 0) + intTotalRead += result; + + return result; + } + + /** + * This needs to support the possibility of a / or a \ separator. + * + * @param strFilename + * the FileSystemName of the file. + * @return the strFilename after removing all characters before the last + * occurence of / or \. + */ + private static final String getBasename(String strFilename) { + if (strFilename == null) + return strFilename; + + int intIndex = strFilename.lastIndexOf("/"); + if (intIndex == -1 || strFilename.lastIndexOf("\\") > intIndex) + intIndex = strFilename.lastIndexOf("\\"); + + if (intIndex != -1) + return strFilename.substring(intIndex + 1); + else + return strFilename; + } + + /** + * Trims any quotes from the start and end of a string. + * + * @param strItem + * @return the trimmed string. + */ + private static final String trimQuotes(String strItem) { + // Saves having to go any further.... + if (strItem == null || strItem.indexOf("\"") == -1) + return strItem; + + // Gets the rid of any whitespace.. + strItem = strItem.trim(); + + if (strItem.charAt(0) == '\"') + strItem = strItem.substring(1); + + if (strItem.charAt(strItem.length() - 1) == '\"') + strItem = strItem.substring(0, strItem.length() - 1); + + return strItem; + } + + /** + * Format of string name=value; name=value; If not found, will return null. + * + * @param strName + * the form field name, used to upload the file. This identifies + * the formfield location in the storage facility. + * @param strToDecode + * + */ + private static final String getValue(String strName, String strToDecode) { + strName = strName + "="; + + int startIndexOf = 0; + while (startIndexOf < strToDecode.length()) { + int indexOf = strToDecode.indexOf(strName, startIndexOf); + // Ensure either first name, or a space or ; precedes it. + if (indexOf != -1) { + if (indexOf == 0 + || Character.isWhitespace(strToDecode + .charAt(indexOf - 1)) + || strToDecode.charAt(indexOf - 1) == ';') { + int endIndexOf = strToDecode.indexOf(";", indexOf + + strName.length()); + if (endIndexOf == -1) // May return an empty string... + return strToDecode + .substring(indexOf + strName.length()); + else + return strToDecode.substring( + indexOf + strName.length(), endIndexOf); + } else + startIndexOf = indexOf + strName.length(); + } else + return null; + } + return null; + } + + /** + * <I>Tomcat's ServletInputStream.readLine(byte[],int,int) Slightly Modified + * to utilise in.read()</I> <BR> + * Reads the input stream, one line at a time. Starting at an offset, reads + * bytes into an array, until it reads a certain number of bytes or reaches + * a newline character, which it reads into the array as well. + * + * <p> + * This method <u><b>does not</b></u> returns -1 if it reaches the end of + * the input stream before reading the maximum number of bytes, it returns + * -1, if no bytes read. + * + * @param in + * the InputStream to read and parse. + * @param b + * an array of bytes into which data is read. + * + * @param off + * an integer specifying the character at which this method + * begins reading. + * + * @param len + * an integer specifying the maximum number of bytes to read. + * + * @return an integer specifying the actual number of bytes read, or -1 if + * the end of the stream is reached. + * + * @throws IOException + * if an input or output exception has occurred + * + * + * Note: We have a problem with Tomcat reporting an erroneous number of + * bytes, so we need to check this. This is the method where we get an + * infinite loop, but only with binary files. + */ + private int readLine(InputStream in, byte[] b, int off, int len) + throws IOException { + if (len <= 0) + return 0; + + int count = 0, c; + + while ((c = in.read()) != -1) { + b[off++] = (byte) c; + count++; + if (c == '\n' || count == len) + break; + } + + return count > 0 ? count : -1; + } + + /** + * Prints the given debugging message. + * + * @param x + * the message to print. + */ + protected void debug(String x) { + if (debug != null) { + debug.println(x); + debug.flush(); + } + } + + /** + * Gets the Html Table.For debugging. + */ + public String getHtmlTable() { + StringBuffer sbReturn = new StringBuffer(); + + sbReturn.append("<h2>Parameters</h2>"); + sbReturn + .append("\n<table border=3><tr><td><b>Name</b></td><td><b>Value</b></td></tr>"); + for (Enumeration e = getParameterNames(); e.hasMoreElements();) { + String strName = (String) e.nextElement(); + sbReturn.append("\n<tr>" + "<td>" + strName + "</td>"); + + sbReturn.append("<td><table border=1><tr>"); + for (Enumeration f = getURLParameters(strName); f.hasMoreElements();) { + String value = (String) f.nextElement(); + sbReturn.append("<td>" + value + "</td>"); + } + sbReturn.append("</tr></table></td></tr>"); + } + sbReturn.append("</table>"); + + sbReturn.append("<h2>File Parameters</h2>"); + + sbReturn + .append("\n<table border=2><tr><td><b>Name</b></td><td><b>Filename</b></td><td><b>Path</b></td><td><b>Content Type</b></td><td><b>Size</b></td></tr>"); + for (Enumeration e = getFileParameterNames(); e.hasMoreElements();) { + String strName = (String) e.nextElement(); + + sbReturn + .append("\n<tr>" + + "<td>" + + strName + + "</td>" + + "<td>" + + (getFileSystemName(strName) != null ? getFileSystemName(strName) + : "") + "</td>"); + + if (loadIntoMemory) + sbReturn.append("<td>" + + (getFileSize(strName) > 0 ? "<i>in memory</i>" : "") + + "</td>"); + else + sbReturn.append("<td>" + + (getFile(strName) != null ? getFile(strName) + .getAbsolutePath() : "") + "</td>"); + + sbReturn + .append("<td>" + + (getContentType(strName) != null ? getContentType(strName) + : "") + + "</td>" + + "<td>" + + (getFileSize(strName) != -1 ? getFileSize(strName) + + "" + : "") + "</td>" + "</tr>"); + } + sbReturn.append("</table>"); + + return sbReturn.toString(); + } +} |