From 62e13a2fe673fd52e813768df57fde0982283d14 Mon Sep 17 00:00:00 2001 From: markt Date: Tue, 17 Nov 2009 20:30:39 +0000 Subject: [PATCH] Update re-packaged commons-fileupload to 1.2.1 Done using svn copy from tags in commons so merging future updates should be easier Changes made/required: - All classes in a single renamed package - Removed unused Util methods (so even more classes were not required) - Removed some deprecated classes There is almost certainly more to be done in terms of pruning to the minimum required. Will so that once Servlet 3 file upload has been implemented. git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@881493 13f79535-47bb-0310-9956-ffa450edef68 --- .../catalina/manager/HTMLManagerServlet.java | 14 +- .../http/fileupload/ByteArrayOutputStream.java | 308 +++++ .../tomcat/util/http/fileupload/Closeable.java | 38 + .../http/fileupload/DeferredFileOutputStream.java | 134 ++- .../{DefaultFileItem.java => DiskFileItem.java} | 443 ++++--- ...leItemFactory.java => DiskFileItemFactory.java} | 95 +- .../util/http/fileupload/DiskFileUpload.java | 206 ---- .../util/http/fileupload/FileCleanerCleanup.java | 81 ++ .../util/http/fileupload/FileCleaningTracker.java | 258 ++++ .../util/http/fileupload/FileDeleteStrategy.java | 156 +++ .../tomcat/util/http/fileupload/FileItem.java | 32 +- .../util/http/fileupload/FileItemFactory.java | 11 +- .../util/http/fileupload/FileItemHeaders.java | 77 ++ .../util/http/fileupload/FileItemHeadersImpl.java | 88 ++ .../http/fileupload/FileItemHeadersSupport.java | 47 + .../util/http/fileupload/FileItemIterator.java | 48 + .../util/http/fileupload/FileItemStream.java | 97 ++ .../tomcat/util/http/fileupload/FileUpload.java | 35 +- .../util/http/fileupload/FileUploadBase.java | 1245 +++++++++++++++----- .../util/http/fileupload/FileUploadException.java | 72 +- .../tomcat/util/http/fileupload/FileUtils.java | 229 ++++ .../tomcat/util/http/fileupload/IOUtils.java | 162 +++ .../util/http/fileupload/LimitedInputStream.java | 155 +++ .../util/http/fileupload/MultipartStream.java | 806 ++++++++----- .../util/http/fileupload/ParameterParser.java | 329 ++++++ .../util/http/fileupload/ProgressListener.java | 34 + .../util/http/fileupload/RequestContext.java | 64 + .../util/http/fileupload/ServletFileUpload.java | 146 +++ .../http/fileupload/ServletRequestContext.java | 107 ++ .../tomcat/util/http/fileupload/Streams.java | 166 +++ .../http/fileupload/ThresholdingOutputStream.java | 18 +- .../tomcat/util/http/fileupload/package.html | 126 +- 32 files changed, 4672 insertions(+), 1155 deletions(-) create mode 100644 java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/Closeable.java rename java/org/apache/tomcat/util/http/fileupload/{DefaultFileItem.java => DiskFileItem.java} (58%) rename java/org/apache/tomcat/util/http/fileupload/{DefaultFileItemFactory.java => DiskFileItemFactory.java} (64%) delete mode 100644 java/org/apache/tomcat/util/http/fileupload/DiskFileUpload.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileCleanerCleanup.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileCleaningTracker.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileDeleteStrategy.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemHeadersImpl.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileUtils.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/IOUtils.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/LimitedInputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ParameterParser.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ProgressListener.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/RequestContext.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ServletFileUpload.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ServletRequestContext.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/Streams.java diff --git a/java/org/apache/catalina/manager/HTMLManagerServlet.java b/java/org/apache/catalina/manager/HTMLManagerServlet.java index aa5576eb2..6054b6288 100644 --- a/java/org/apache/catalina/manager/HTMLManagerServlet.java +++ b/java/org/apache/catalina/manager/HTMLManagerServlet.java @@ -49,7 +49,8 @@ import org.apache.catalina.manager.util.SessionUtils; import org.apache.catalina.util.RequestUtil; import org.apache.catalina.util.ServerInfo; import org.apache.catalina.util.URLEncoder; -import org.apache.tomcat.util.http.fileupload.DiskFileUpload; +import org.apache.tomcat.util.http.fileupload.DiskFileItemFactory; +import org.apache.tomcat.util.http.fileupload.ServletFileUpload; import org.apache.tomcat.util.http.fileupload.FileItem; /** @@ -260,15 +261,18 @@ public final class HTMLManagerServlet extends ManagerServlet { protected String upload(HttpServletRequest request) throws IOException { String message = ""; - // Create a new file upload handler - DiskFileUpload upload = new DiskFileUpload(); - // Get the tempdir File tempdir = (File) getServletContext().getAttribute (ServletContext.TEMPDIR); + + // Create a new file upload handler + DiskFileItemFactory factory = new DiskFileItemFactory(); + factory.setRepository(tempdir.getCanonicalFile()); + ServletFileUpload upload = new ServletFileUpload(); + upload.setFileItemFactory(factory); + // Set upload parameters upload.setSizeMax(-1); - upload.setRepositoryPath(tempdir.getCanonicalPath()); // Parse the request String basename = null; diff --git a/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java new file mode 100644 index 000000000..b652d9dcd --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java @@ -0,0 +1,308 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class implements an output stream in which the data is + * written into a byte array. The buffer automatically grows as data + * is written to it. + *

+ * The data can be retrieved using toByteArray() and + * toString(). + *

+ * Closing a ByteArrayOutputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ * This is an alternative implementation of the java.io.ByteArrayOutputStream + * class. The original implementation only allocates 32 bytes at the beginning. + * As this class is designed for heavy duty it starts at 1024 bytes. In contrast + * to the original it doesn't reallocate the whole memory block but allocates + * additional buffers. This way no buffers need to be garbage collected and + * the contents don't have to be copied to the new buffer. This class is + * designed to behave exactly like the original. The only exception is the + * deprecated toString(int) method that has been ignored. + * + * @author Jeremias Maerki + * @author Holger Hoffstatte + * @version $Id$ + */ +public class ByteArrayOutputStream extends OutputStream { + + /** A singleton empty byte array. */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** The list of buffers, which grows and never reduces. */ + private List buffers = new ArrayList(); + /** The index of the current buffer. */ + private int currentBufferIndex; + /** The total count of bytes in all the filled buffers. */ + private int filledBufferSum; + /** The current buffer. */ + private byte[] currentBuffer; + /** The total count of bytes written. */ + private int count; + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 1024 bytes, though its size increases if necessary. + */ + public ByteArrayOutputStream() { + this(1024); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size + * @throws IllegalArgumentException if size is negative + */ + public ByteArrayOutputStream(int size) { + if (size < 0) { + throw new IllegalArgumentException( + "Negative initial size: " + size); + } + needNewBuffer(size); + } + + /** + * Return the appropriate byte[] buffer + * specified by index. + * + * @param index the index of the buffer required + * @return the buffer + */ + private byte[] getBuffer(int index) { + return (byte[]) buffers.get(index); + } + + /** + * Makes a new buffer available either by allocating + * a new one or re-cycling an existing one. + * + * @param newcount the size of the buffer if one is created + */ + private void needNewBuffer(int newcount) { + if (currentBufferIndex < buffers.size() - 1) { + //Recycling old buffer + filledBufferSum += currentBuffer.length; + + currentBufferIndex++; + currentBuffer = getBuffer(currentBufferIndex); + } else { + //Creating new buffer + int newBufferSize; + if (currentBuffer == null) { + newBufferSize = newcount; + filledBufferSum = 0; + } else { + newBufferSize = Math.max( + currentBuffer.length << 1, + newcount - filledBufferSum); + filledBufferSum += currentBuffer.length; + } + + currentBufferIndex++; + currentBuffer = new byte[newBufferSize]; + buffers.add(currentBuffer); + } + } + + /** + * Write the bytes to byte array. + * @param b the bytes to write + * @param off The start offset + * @param len The number of bytes to write + */ + public void write(byte[] b, int off, int len) { + if ((off < 0) + || (off > b.length) + || (len < 0) + || ((off + len) > b.length) + || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + synchronized (this) { + int newcount = count + len; + int remaining = len; + int inBufferPos = count - filledBufferSum; + while (remaining > 0) { + int part = Math.min(remaining, currentBuffer.length - inBufferPos); + System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); + remaining -= part; + if (remaining > 0) { + needNewBuffer(newcount); + inBufferPos = 0; + } + } + count = newcount; + } + } + + /** + * Write a byte to byte array. + * @param b the byte to write + */ + public synchronized void write(int b) { + int inBufferPos = count - filledBufferSum; + if (inBufferPos == currentBuffer.length) { + needNewBuffer(count + 1); + inBufferPos = 0; + } + currentBuffer[inBufferPos] = (byte) b; + count++; + } + + /** + * Writes the entire contents of the specified input stream to this + * byte stream. Bytes from the input stream are read directly into the + * internal buffers of this streams. + * + * @param in the input stream to read from + * @return total number of bytes read from the input stream + * (and written to this stream) + * @throws IOException if an I/O error occurs while reading the input stream + * @since Commons IO 1.4 + */ + public synchronized int write(InputStream in) throws IOException { + int readCount = 0; + int inBufferPos = count - filledBufferSum; + int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); + while (n != -1) { + readCount += n; + inBufferPos += n; + count += n; + if (inBufferPos == currentBuffer.length) { + needNewBuffer(currentBuffer.length); + inBufferPos = 0; + } + n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); + } + return readCount; + } + + /** + * Return the current size of the byte array. + * @return the current size of the byte array + */ + public synchronized int size() { + return count; + } + + /** + * Closing a ByteArrayOutputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + * + * @throws IOException never (this method should not declare this exception + * but it has to now due to backwards compatability) + */ + public void close() throws IOException { + //nop + } + + /** + * @see java.io.ByteArrayOutputStream#reset() + */ + public synchronized void reset() { + count = 0; + filledBufferSum = 0; + currentBufferIndex = 0; + currentBuffer = getBuffer(currentBufferIndex); + } + + /** + * Writes the entire contents of this byte stream to the + * specified output stream. + * + * @param out the output stream to write to + * @throws IOException if an I/O error occurs, such as if the stream is closed + * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) + */ + public synchronized void writeTo(OutputStream out) throws IOException { + int remaining = count; + for (int i = 0; i < buffers.size(); i++) { + byte[] buf = getBuffer(i); + int c = Math.min(buf.length, remaining); + out.write(buf, 0, c); + remaining -= c; + if (remaining == 0) { + break; + } + } + } + + /** + * Gets the curent contents of this byte stream as a byte array. + * The result is independent of this stream. + * + * @return the current contents of this output stream, as a byte array + * @see java.io.ByteArrayOutputStream#toByteArray() + */ + public synchronized byte[] toByteArray() { + int remaining = count; + if (remaining == 0) { + return EMPTY_BYTE_ARRAY; + } + byte newbuf[] = new byte[remaining]; + int pos = 0; + for (int i = 0; i < buffers.size(); i++) { + byte[] buf = getBuffer(i); + int c = Math.min(buf.length, remaining); + System.arraycopy(buf, 0, newbuf, pos, c); + pos += c; + remaining -= c; + if (remaining == 0) { + break; + } + } + return newbuf; + } + + /** + * Gets the curent contents of this byte stream as a string. + * @return the contents of the byte array as a String + * @see java.io.ByteArrayOutputStream#toString() + */ + public String toString() { + return new String(toByteArray()); + } + + /** + * Gets the curent contents of this byte stream as a string + * using the specified encoding. + * + * @param enc the name of the character encoding + * @return the string converted from the byte array + * @throws UnsupportedEncodingException if the encoding is not supported + * @see java.io.ByteArrayOutputStream#toString(String) + */ + public String toString(String enc) throws UnsupportedEncodingException { + return new String(toByteArray(), enc); + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/Closeable.java b/java/org/apache/tomcat/util/http/fileupload/Closeable.java new file mode 100644 index 000000000..04d877b58 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/Closeable.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; + + +/** + * Interface of an object, which may be closed. + */ +public interface Closeable { + /** + * Closes the object. + * @throws IOException An I/O error occurred. + */ + void close() throws IOException; + + /** + * Returns, whether the object is already closed. + * @return True, if the object is closed, otherwise false. + * @throws IOException An I/O error occurred. + */ + boolean isClosed() throws IOException; +} diff --git a/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java index 8c1e4c0dd..bb21d4ecc 100644 --- a/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java +++ b/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java @@ -14,23 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.tomcat.util.http.fileupload; -import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; + /** - *

An output stream which will retain data in memory until a specified + * An output stream which will retain data in memory until a specified * threshold is reached, and only then commit it to disk. If the stream is * closed before the threshold is reached, the data will not be written to - * disk at all.

+ * disk at all. + *

+ * This class originated in FileUpload processing. In this use case, you do + * not know in advance the size of the file being uploaded. If the file is small + * you want to store it in memory (for speed), but if the file is large you want + * to store it to file (to avoid memory issues). * * @author Martin Cooper + * @author gaxzerow * * @version $Id$ */ @@ -42,7 +47,7 @@ public class DeferredFileOutputStream /** - * The output stream to which data will be written prior to the threshold + * The output stream to which data will be written prior to the theshold * being reached. */ private ByteArrayOutputStream memoryOutputStream; @@ -61,6 +66,26 @@ public class DeferredFileOutputStream */ private File outputFile; + /** + * The temporary file prefix. + */ + private String prefix; + + /** + * The temporary file suffix. + */ + private String suffix; + + /** + * The directory to use for temporary files. + */ + private File directory; + + + /** + * True when close() has been called successfully. + */ + private boolean closed = false; // ----------------------------------------------------------- Constructors @@ -77,18 +102,34 @@ public class DeferredFileOutputStream super(threshold); this.outputFile = outputFile; - if (threshold < DefaultFileItemFactory.DEFAULT_SIZE_THRESHOLD) { - // Small threshold, use it - memoryOutputStream = new ByteArrayOutputStream(threshold); - } else { - // Large threshold. Use default and array will expand if required - memoryOutputStream = new ByteArrayOutputStream( - DefaultFileItemFactory.DEFAULT_SIZE_THRESHOLD); - } + memoryOutputStream = new ByteArrayOutputStream(); currentOutputStream = memoryOutputStream; } + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold, and save data to a temporary file beyond that point. + * + * @param threshold The number of bytes at which to trigger an event. + * @param prefix Prefix to use for the temporary file. + * @param suffix Suffix to use for the temporary file. + * @param directory Temporary file directory. + * + * @since Commons IO 1.4 + */ + public DeferredFileOutputStream(int threshold, String prefix, String suffix, File directory) + { + this(threshold, (File)null); + if (prefix == null) { + throw new IllegalArgumentException("Temporary file prefix is missing"); + } + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + // --------------------------------------- ThresholdingOutputStream methods @@ -100,7 +141,6 @@ public class DeferredFileOutputStream * * @exception IOException if an error occurs. */ - @Override protected OutputStream getStream() throws IOException { return currentOutputStream; @@ -115,12 +155,13 @@ public class DeferredFileOutputStream * * @exception IOException if an error occurs. */ - @Override protected void thresholdReached() throws IOException { - byte[] data = memoryOutputStream.toByteArray(); + if (prefix != null) { + outputFile = File.createTempFile(prefix, suffix, directory); + } FileOutputStream fos = new FileOutputStream(outputFile); - fos.write(data); + memoryOutputStream.writeTo(fos); currentOutputStream = fos; memoryOutputStream = null; } @@ -161,9 +202,15 @@ public class DeferredFileOutputStream /** - * Returns the data for this output stream as a File, assuming - * that the data was written to disk. If the data was retained in memory, - * this method returns null. + * Returns either the output file specified in the constructor or + * the temporary file created or null. + *

+ * If the constructor specifying the file is used then it returns that + * same output file, even when threashold has not been reached. + *

+ * If constructor specifying a temporary file prefix/suffix is used + * then the temporary file created once the threashold is reached is returned + * If the threshold was not reached then null is returned. * * @return The file for this output stream, or null if no such * file exists. @@ -172,4 +219,49 @@ public class DeferredFileOutputStream { return outputFile; } + + + /** + * Closes underlying output stream, and mark this as closed + * + * @exception IOException if an error occurs. + */ + public void close() throws IOException + { + super.close(); + closed = true; + } + + + /** + * Writes the data from this output stream to the specified output stream, + * after it has been closed. + * + * @param out output stream to write to. + * @exception IOException if this stream is not yet closed or an error occurs. + */ + public void writeTo(OutputStream out) throws IOException + { + // we may only need to check if this is closed if we are working with a file + // but we should force the habit of closing wether we are working with + // a file or memory. + if (!closed) + { + throw new IOException("Stream not closed"); + } + + if(isInMemory()) + { + memoryOutputStream.writeTo(out); + } + else + { + FileInputStream fis = new FileInputStream(outputFile); + try { + IOUtils.copy(fis, out); + } finally { + IOUtils.closeQuietly(fis); + } + } + } } diff --git a/java/org/apache/tomcat/util/http/fileupload/DefaultFileItem.java b/java/org/apache/tomcat/util/http/fileupload/DiskFileItem.java similarity index 58% rename from java/org/apache/tomcat/util/http/fileupload/DefaultFileItem.java rename to java/org/apache/tomcat/util/http/fileupload/DiskFileItem.java index 65218dd32..719ae9368 100644 --- a/java/org/apache/tomcat/util/http/fileupload/DefaultFileItem.java +++ b/java/org/apache/tomcat/util/http/fileupload/DiskFileItem.java @@ -5,20 +5,17 @@ * 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.tomcat.util.http.fileupload; - import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -27,23 +24,37 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.util.Map; /** *

The default implementation of the - * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface. + * {@link org.apache.commons.fileupload.FileItem FileItem} interface. * *

After retrieving an instance of this class from a {@link - * org.apache.tomcat.util.http.fileupload.DiskFileUpload DiskFileUpload} instance (see - * {@link org.apache.tomcat.util.http.fileupload.DiskFileUpload + * org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} instance (see + * {@link org.apache.commons.fileupload.DiskFileUpload * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may * either request all contents of file at once using {@link #get()} or * request an {@link java.io.InputStream InputStream} with * {@link #getInputStream()} and process the file without attempting to load * it into memory, which may come handy with large files. * + *

When using the DiskFileItemFactory, then you should + * consider the following: Temporary files are automatically deleted as + * soon as they are no longer needed. (More precisely, when the + * corresponding instance of {@link java.io.File} is garbage collected.) + * This is done by the so-called reaper thread, which is started + * automatically when the class {@link org.apache.commons.io.FileCleaner} + * is loaded. + * It might make sense to terminate that thread, for example, if + * your web application ends. See the section on "Resource cleanup" + * in the users guide of commons-fileupload.

+ * * @author Rafal Krzewski * @author Sean Legassick * @author Jason van Zyl @@ -51,16 +62,41 @@ import java.io.UnsupportedEncodingException; * @author Martin Cooper * @author Sean C. Sullivan * + * @since FileUpload 1.1 + * * @version $Id$ */ -public class DefaultFileItem - implements FileItem -{ +public class DiskFileItem + implements FileItem, FileItemHeadersSupport { + + // ----------------------------------------------------- Manifest constants + + /** + * The UID to use when serializing this instance. + */ + private static final long serialVersionUID = 2237570099615271025L; + + + /** + * Default content charset to be used when no explicit charset + * parameter is provided by the sender. Media subtypes of the + * "text" type are defined to have a default charset value of + * "ISO-8859-1" when received via HTTP. + */ + public static final String DEFAULT_CHARSET = "ISO-8859-1"; + // ----------------------------------------------------------- Data members /** + * UID used in unique file name generation. + */ + private static final String UID = + new java.rmi.server.UID().toString() + .replace(':', '_').replace('-', '_'); + + /** * Counter used in unique identifier generation. */ private static int counter = 0; @@ -92,6 +128,13 @@ public class DefaultFileItem /** + * The size of the item, in bytes. This is used to cache the size when a + * file item is moved from its original location. + */ + private long size = -1; + + + /** * The threshold above which uploads will be stored on disk. */ private int sizeThreshold; @@ -112,14 +155,28 @@ public class DefaultFileItem /** * Output stream for this item. */ - private DeferredFileOutputStream dfos; + private transient DeferredFileOutputStream dfos; + /** + * The temporary file to use. + */ + private transient File tempFile; + + /** + * File to allow for serialization of the content of this item. + */ + private File dfosFile; + + /** + * The file items headers. + */ + private FileItemHeaders headers; // ----------------------------------------------------------- Constructors /** - * Constructs a new DefaultFileItem instance. + * Constructs a new DiskFileItem instance. * * @param fieldName The name of the form field. * @param contentType The content type passed by the browser or @@ -135,9 +192,9 @@ public class DefaultFileItem * which files will be created, should the item size * exceed the threshold. */ - DefaultFileItem(String fieldName, String contentType, boolean isFormField, - String fileName, int sizeThreshold, File repository) - { + public DiskFileItem(String fieldName, + String contentType, boolean isFormField, String fileName, + int sizeThreshold, File repository) { this.fieldName = fieldName; this.contentType = contentType; this.isFormField = isFormField; @@ -157,18 +214,15 @@ public class DefaultFileItem * @return An {@link java.io.InputStream InputStream} that can be * used to retrieve the contents of the file. * - * @exception IOException if an error occurs. + * @throws IOException if an error occurs. */ public InputStream getInputStream() - throws IOException - { - if (!dfos.isInMemory()) - { + throws IOException { + if (!isInMemory()) { return new FileInputStream(dfos.getFile()); } - if (cachedContent == null) - { + if (cachedContent == null) { cachedContent = dfos.getData(); } return new ByteArrayInputStream(cachedContent); @@ -176,25 +230,39 @@ public class DefaultFileItem /** - * Returns the content type passed by the browser or null if + * Returns the content type passed by the agent or null if * not defined. * - * @return The content type passed by the browser or null if + * @return The content type passed by the agent or null if * not defined. */ - public String getContentType() - { + public String getContentType() { return contentType; } /** + * Returns the content charset passed by the agent or null if + * not defined. + * + * @return The content charset passed by the agent or null if + * not defined. + */ + public String getCharSet() { + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = parser.parse(getContentType(), ';'); + return (String) params.get("charset"); + } + + + /** * Returns the original filename in the client's filesystem. * * @return The original filename in the client's filesystem. */ - public String getName() - { + public String getName() { return fileName; } @@ -209,9 +277,11 @@ public class DefaultFileItem * @return true if the file contents will be read * from memory; false otherwise. */ - public boolean isInMemory() - { - return (dfos.isInMemory()); + public boolean isInMemory() { + if (cachedContent != null) { + return true; + } + return dfos.isInMemory(); } @@ -220,18 +290,14 @@ public class DefaultFileItem * * @return The size of the file, in bytes. */ - public long getSize() - { - if (cachedContent != null) - { + public long getSize() { + if (size >= 0) { + return size; + } else if (cachedContent != null) { return cachedContent.length; - } - else if (dfos.isInMemory()) - { + } else if (dfos.isInMemory()) { return dfos.getData().length; - } - else - { + } else { return dfos.getFile().length(); } } @@ -244,12 +310,9 @@ public class DefaultFileItem * * @return The contents of the file as an array of bytes. */ - public byte[] get() - { - if (dfos.isInMemory()) - { - if (cachedContent == null) - { + public byte[] get() { + if (isInMemory()) { + if (cachedContent == null) { cachedContent = dfos.getData(); } return cachedContent; @@ -258,25 +321,16 @@ public class DefaultFileItem byte[] fileData = new byte[(int) getSize()]; FileInputStream fis = null; - try - { + try { fis = new FileInputStream(dfos.getFile()); fis.read(fileData); - } - catch (IOException e) - { + } catch (IOException e) { fileData = null; - } - finally - { - if (fis != null) - { - try - { + } finally { + if (fis != null) { + try { fis.close(); - } - catch (IOException e) - { + } catch (IOException e) { // ignore } } @@ -291,17 +345,16 @@ public class DefaultFileItem * encoding. This method uses {@link #get()} to retrieve the * contents of the file. * - * @param encoding The character encoding to use. + * @param charset The charset to use. * * @return The contents of the file, as a string. * - * @exception UnsupportedEncodingException if the requested character - * encoding is not available. + * @throws UnsupportedEncodingException if the requested character + * encoding is not available. */ - public String getString(String encoding) - throws UnsupportedEncodingException - { - return new String(get(), encoding); + public String getString(final String charset) + throws UnsupportedEncodingException { + return new String(get(), charset); } @@ -311,10 +364,20 @@ public class DefaultFileItem * contents of the file. * * @return The contents of the file, as a string. + * + * @todo Consider making this method throw UnsupportedEncodingException. */ - public String getString() - { - return new String(get()); + public String getString() { + byte[] rawdata = get(); + String charset = getCharSet(); + if (charset == null) { + charset = DEFAULT_CHARSET; + } + try { + return new String(rawdata, charset); + } catch (UnsupportedEncodingException e) { + return new String(rawdata); + } } @@ -336,76 +399,56 @@ public class DefaultFileItem * @param file The File into which the uploaded item should * be stored. * - * @exception Exception if an error occurs. + * @throws Exception if an error occurs. */ - public void write(File file) throws Exception - { - if (isInMemory()) - { + public void write(File file) throws Exception { + if (isInMemory()) { FileOutputStream fout = null; - try - { + try { fout = new FileOutputStream(file); fout.write(get()); - } - finally - { - if (fout != null) - { + } finally { + if (fout != null) { fout.close(); } } - } - else - { + } else { File outputFile = getStoreLocation(); - if (outputFile != null) - { + if (outputFile != null) { + // Save the length of the file + size = outputFile.length(); /* * The uploaded file is being stored on disk * in a temporary location so move it to the * desired file. */ - if (!outputFile.renameTo(file)) - { + if (!outputFile.renameTo(file)) { BufferedInputStream in = null; BufferedOutputStream out = null; - try - { + try { in = new BufferedInputStream( new FileInputStream(outputFile)); out = new BufferedOutputStream( new FileOutputStream(file)); - byte[] bytes = new byte[2048]; - int s = 0; - while ((s = in.read(bytes)) != -1) - { - out.write(bytes, 0, s); - } - } - finally - { - try - { - in.close(); - } - catch (IOException e) - { - // ignore + IOUtils.copy(in, out); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // ignore + } } - try - { - out.close(); - } - catch (IOException e) - { - // ignore + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // ignore + } } } } - } - else - { + } else { /* * For whatever reason we cannot write the * file to disk. @@ -424,12 +467,10 @@ public class DefaultFileItem * collected, this method can be used to ensure that this is done at an * earlier time, thus preserving system resources. */ - public void delete() - { + public void delete() { cachedContent = null; File outputFile = getStoreLocation(); - if (outputFile != null && outputFile.exists()) - { + if (outputFile != null && outputFile.exists()) { outputFile.delete(); } } @@ -444,8 +485,7 @@ public class DefaultFileItem * @see #setFieldName(java.lang.String) * */ - public String getFieldName() - { + public String getFieldName() { return fieldName; } @@ -458,8 +498,7 @@ public class DefaultFileItem * @see #getFieldName() * */ - public void setFieldName(String fieldName) - { + public void setFieldName(String fieldName) { this.fieldName = fieldName; } @@ -474,8 +513,7 @@ public class DefaultFileItem * @see #setFormField(boolean) * */ - public boolean isFormField() - { + public boolean isFormField() { return isFormField; } @@ -490,8 +528,7 @@ public class DefaultFileItem * @see #isFormField() * */ - public void setFormField(boolean state) - { + public void setFormField(boolean state) { isFormField = state; } @@ -503,13 +540,11 @@ public class DefaultFileItem * @return An {@link java.io.OutputStream OutputStream} that can be used * for storing the contensts of the file. * - * @exception IOException if an error occurs. + * @throws IOException if an error occurs. */ public OutputStream getOutputStream() - throws IOException - { - if (dfos == null) - { + throws IOException { + if (dfos == null) { File outputFile = getTempFile(); dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); } @@ -533,9 +568,8 @@ public class DefaultFileItem * @return The data file, or null if the data is stored in * memory. */ - public File getStoreLocation() - { - return dfos.getFile(); + public File getStoreLocation() { + return dfos == null ? null : dfos.getFile(); } @@ -545,13 +579,10 @@ public class DefaultFileItem /** * Removes the file contents from the temporary storage. */ - @Override - protected void finalize() - { + protected void finalize() { File outputFile = dfos.getFile(); - if (outputFile != null && outputFile.exists()) - { + if (outputFile != null && outputFile.exists()) { outputFile.delete(); } } @@ -559,23 +590,25 @@ public class DefaultFileItem /** * Creates and returns a {@link java.io.File File} representing a uniquely - * named temporary file in the configured repository path. + * named temporary file in the configured repository path. The lifetime of + * the file is tied to the lifetime of the FileItem instance; + * the file will be deleted when the instance is garbage collected. * * @return The {@link java.io.File File} to be used for temporary storage. */ - protected File getTempFile() - { - File tempDir = repository; - if (tempDir == null) - { - tempDir = new File(System.getProperty("java.io.tmpdir")); - } + protected File getTempFile() { + if (tempFile == null) { + File tempDir = repository; + if (tempDir == null) { + tempDir = new File(System.getProperty("java.io.tmpdir")); + } - String fileName = "upload_" + getUniqueId() + ".tmp"; + String tempFileName = + "upload_" + UID + "_" + getUniqueId() + ".tmp"; - File f = new File(tempDir, fileName); - f.deleteOnExit(); - return f; + tempFile = new File(tempDir, tempFileName); + } + return tempFile; } @@ -588,22 +621,106 @@ public class DefaultFileItem * * @return A String with the non-random looking instance identifier. */ - private static String getUniqueId() - { + private static String getUniqueId() { + final int limit = 100000000; int current; - synchronized (DefaultFileItem.class) - { + synchronized (DiskFileItem.class) { current = counter++; } String id = Integer.toString(current); // If you manage to get more than 100 million of ids, you'll // start getting ids longer than 8 characters. - if (current < 100000000) - { + if (current < limit) { id = ("00000000" + id).substring(id.length()); } return id; } + + + + /** + * Returns a string representation of this object. + * + * @return a string representation of this object. + */ + public String toString() { + return "name=" + this.getName() + + ", StoreLocation=" + + String.valueOf(this.getStoreLocation()) + + ", size=" + + this.getSize() + + "bytes, " + + "isFormField=" + isFormField() + + ", FieldName=" + + this.getFieldName(); + } + + + // -------------------------------------------------- Serialization methods + + + /** + * Writes the state of this object during serialization. + * + * @param out The stream to which the state should be written. + * + * @throws IOException if an error occurs. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + // Read the data + if (dfos.isInMemory()) { + cachedContent = get(); + } else { + cachedContent = null; + dfosFile = dfos.getFile(); + } + + // write out values + out.defaultWriteObject(); + } + + /** + * Reads the state of this object during deserialization. + * + * @param in The stream from which the state should be read. + * + * @throws IOException if an error occurs. + * @throws ClassNotFoundException if class cannot be found. + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + // read values + in.defaultReadObject(); + + OutputStream output = getOutputStream(); + if (cachedContent != null) { + output.write(cachedContent); + } else { + FileInputStream input = new FileInputStream(dfosFile); + IOUtils.copy(input, output); + dfosFile.delete(); + dfosFile = null; + } + output.close(); + + cachedContent = null; + } + + /** + * Returns the file item headers. + * @return The file items headers. + */ + public FileItemHeaders getHeaders() { + return headers; + } + + /** + * Sets the file item headers. + * @param pHeaders The file items headers. + */ + public void setHeaders(FileItemHeaders pHeaders) { + headers = pHeaders; + } } diff --git a/java/org/apache/tomcat/util/http/fileupload/DefaultFileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/DiskFileItemFactory.java similarity index 64% rename from java/org/apache/tomcat/util/http/fileupload/DefaultFileItemFactory.java rename to java/org/apache/tomcat/util/http/fileupload/DiskFileItemFactory.java index 2d29b3899..1dc8f2b45 100644 --- a/java/org/apache/tomcat/util/http/fileupload/DefaultFileItemFactory.java +++ b/java/org/apache/tomcat/util/http/fileupload/DiskFileItemFactory.java @@ -5,26 +5,24 @@ * 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.tomcat.util.http.fileupload; import java.io.File; /** - *

The default {@link org.apache.tomcat.util.http.fileupload.FileItemFactory} + *

The default {@link org.apache.commons.fileupload.FileItemFactory} * implementation. This implementation creates - * {@link org.apache.tomcat.util.http.fileupload.FileItem} instances which keep their + * {@link org.apache.commons.fileupload.FileItem} instances which keep their * content either in memory, for smaller items, or in a temporary file on disk, * for larger items. The size threshold, above which content will be stored on * disk, is configurable, as is the directory in which temporary files will be @@ -39,12 +37,24 @@ import java.io.File; * *

* + *

When using the DiskFileItemFactory, then you should + * consider the following: Temporary files are automatically deleted as + * soon as they are no longer needed. (More precisely, when the + * corresponding instance of {@link java.io.File} is garbage collected.) + * Cleaning up those files is done by an instance of + * {@link FileCleaningTracker}, and an associated thread. In a complex + * environment, for example in a web application, you should consider + * terminating this thread, for example, when your web application + * ends. See the section on "Resource cleanup" + * in the users guide of commons-fileupload.

+ * * @author Martin Cooper * + * @since FileUpload 1.1 + * * @version $Id$ */ -public class DefaultFileItemFactory implements FileItemFactory -{ +public class DiskFileItemFactory implements FileItemFactory { // ----------------------------------------------------- Manifest constants @@ -70,6 +80,13 @@ public class DefaultFileItemFactory implements FileItemFactory private int sizeThreshold = DEFAULT_SIZE_THRESHOLD; + /** + *

The instance of {@link FileCleaningTracker}, which is responsible + * for deleting temporary files.

+ *

May be null, if tracking files is not required.

+ */ + private FileCleaningTracker fileCleaningTracker; + // ----------------------------------------------------------- Constructors @@ -77,8 +94,8 @@ public class DefaultFileItemFactory implements FileItemFactory * Constructs an unconfigured instance of this class. The resulting factory * may be configured by calling the appropriate setter methods. */ - public DefaultFileItemFactory() - { + public DiskFileItemFactory() { + this(DEFAULT_SIZE_THRESHOLD, null); } @@ -92,13 +109,11 @@ public class DefaultFileItemFactory implements FileItemFactory * which files will be created, should the item size * exceed the threshold. */ - public DefaultFileItemFactory(int sizeThreshold, File repository) - { + public DiskFileItemFactory(int sizeThreshold, File repository) { this.sizeThreshold = sizeThreshold; this.repository = repository; } - // ------------------------------------------------------------- Properties @@ -111,8 +126,7 @@ public class DefaultFileItemFactory implements FileItemFactory * @see #setRepository(java.io.File) * */ - public File getRepository() - { + public File getRepository() { return repository; } @@ -126,22 +140,20 @@ public class DefaultFileItemFactory implements FileItemFactory * @see #getRepository() * */ - public void setRepository(File repository) - { + public void setRepository(File repository) { this.repository = repository; } /** * Returns the size threshold beyond which files are written directly to - * disk. The default value is 1024 bytes. + * disk. The default value is 10240 bytes. * * @return The size threshold, in bytes. * * @see #setSizeThreshold(int) */ - public int getSizeThreshold() - { + public int getSizeThreshold() { return sizeThreshold; } @@ -154,8 +166,7 @@ public class DefaultFileItemFactory implements FileItemFactory * @see #getSizeThreshold() * */ - public void setSizeThreshold(int sizeThreshold) - { + public void setSizeThreshold(int sizeThreshold) { this.sizeThreshold = sizeThreshold; } @@ -163,7 +174,7 @@ public class DefaultFileItemFactory implements FileItemFactory // --------------------------------------------------------- Public Methods /** - * Create a new {@link org.apache.tomcat.util.http.fileupload.DefaultFileItem} + * Create a new {@link org.apache.commons.fileupload.disk.DiskFileItem} * instance from the supplied parameters and the local factory * configuration. * @@ -176,15 +187,37 @@ public class DefaultFileItemFactory implements FileItemFactory * * @return The newly created file item. */ - public FileItem createItem( - String fieldName, - String contentType, - boolean isFormField, - String fileName - ) - { - return new DefaultFileItem(fieldName, contentType, + public FileItem createItem(String fieldName, String contentType, + boolean isFormField, String fileName) { + DiskFileItem result = new DiskFileItem(fieldName, contentType, isFormField, fileName, sizeThreshold, repository); + FileCleaningTracker tracker = getFileCleaningTracker(); + if (tracker != null) { + tracker.track(result.getTempFile(), this); + } + return result; } + + /** + * Returns the tracker, which is responsible for deleting temporary + * files. + * @return An instance of {@link FileCleaningTracker}, defaults to + * {@link org.apache.commons.io.FileCleaner#getInstance()}. Null, + * if temporary files aren't tracked. + */ + public FileCleaningTracker getFileCleaningTracker() { + return fileCleaningTracker; + } + + /** + * Returns the tracker, which is responsible for deleting temporary + * files. + * @param pTracker An instance of {@link FileCleaningTracker}, + * which will from now on track the created files. May be null + * to disable tracking. + */ + public void setFileCleaningTracker(FileCleaningTracker pTracker) { + fileCleaningTracker = pTracker; + } } diff --git a/java/org/apache/tomcat/util/http/fileupload/DiskFileUpload.java b/java/org/apache/tomcat/util/http/fileupload/DiskFileUpload.java deleted file mode 100644 index a514e0f78..000000000 --- a/java/org/apache/tomcat/util/http/fileupload/DiskFileUpload.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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.tomcat.util.http.fileupload; - - -import java.io.File; -import java.util.List; -import javax.servlet.http.HttpServletRequest; - - -/** - *

High level API for processing file uploads.

- * - *

This class handles multiple files per single HTML widget, sent using - * multipart/mixed encoding type, as specified by - * RFC 1867. Use {@link - * #parseRequest(HttpServletRequest)} to acquire a list of {@link - * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML - * widget.

- * - *

Individual parts will be stored in temporary disk storage or in memory, - * depending on their size, and will be available as {@link - * org.apache.tomcat.util.http.fileupload.FileItem}s.

- * - * @author Rafal Krzewski - * @author Daniel Rall - * @author Jason van Zyl - * @author John McNally - * @author Martin Cooper - * @author Sean C. Sullivan - * - * @version $Id$ - */ -public class DiskFileUpload - extends FileUploadBase - { - - // ----------------------------------------------------------- Data members - - - /** - * The factory to use to create new form items. - */ - private DefaultFileItemFactory fileItemFactory; - - - // ----------------------------------------------------------- Constructors - - - /** - * Constructs an instance of this class which uses the default factory to - * create FileItem instances. - * - * @see #DiskFileUpload(DefaultFileItemFactory fileItemFactory) - */ - public DiskFileUpload() - { - super(); - this.fileItemFactory = new DefaultFileItemFactory(); - } - - - /** - * Constructs an instance of this class which uses the supplied factory to - * create FileItem instances. - * - * @see #DiskFileUpload() - */ - public DiskFileUpload(DefaultFileItemFactory fileItemFactory) - { - super(); - this.fileItemFactory = fileItemFactory; - } - - - // ----------------------------------------------------- Property accessors - - - /** - * Returns the factory class used when creating file items. - * - * @return The factory class for new file items. - */ - @Override - public FileItemFactory getFileItemFactory() - { - return fileItemFactory; - } - - - /** - * Sets the factory class to use when creating file items. The factory must - * be an instance of DefaultFileItemFactory or a subclass - * thereof, or else a ClassCastException will be thrown. - * - * @param factory The factory class for new file items. - */ - @Override - public void setFileItemFactory(FileItemFactory factory) - { - this.fileItemFactory = (DefaultFileItemFactory) factory; - } - - - /** - * Returns the size threshold beyond which files are written directly to - * disk. - * - * @return The size threshold, in bytes. - * - * @see #setSizeThreshold(int) - */ - public int getSizeThreshold() - { - return fileItemFactory.getSizeThreshold(); - } - - - /** - * Sets the size threshold beyond which files are written directly to disk. - * - * @param sizeThreshold The size threshold, in bytes. - * - * @see #getSizeThreshold() - */ - public void setSizeThreshold(int sizeThreshold) - { - fileItemFactory.setSizeThreshold(sizeThreshold); - } - - - /** - * Returns the location used to temporarily store files that are larger - * than the configured size threshold. - * - * @return The path to the temporary file location. - * - * @see #setRepositoryPath(String) - */ - public String getRepositoryPath() - { - return fileItemFactory.getRepository().getPath(); - } - - - /** - * Sets the location used to temporarily store files that are larger - * than the configured size threshold. - * - * @param repositoryPath The path to the temporary file location. - * - * @see #getRepositoryPath() - */ - public void setRepositoryPath(String repositoryPath) - { - fileItemFactory.setRepository(new File(repositoryPath)); - } - - - // --------------------------------------------------------- Public methods - - - /** - * Processes an RFC 1867 - * compliant multipart/form-data stream. If files are stored - * on disk, the path is given by getRepository(). - * - * @param req The servlet request to be parsed. Must be non-null. - * @param sizeThreshold The max size in bytes to be stored in memory. - * @param sizeMax The maximum allowed upload size, in bytes. - * @param path The location where the files should be stored. - * - * @return A list of FileItem instances parsed from the - * request, in the order that they were transmitted. - * - * @exception FileUploadException if there are problems reading/parsing - * the request or storing files. - */ - public List parseRequest(HttpServletRequest req, - int sizeThreshold, - long sizeMax, String path) - throws FileUploadException - { - setSizeThreshold(sizeThreshold); - setSizeMax(sizeMax); - setRepositoryPath(path); - return parseRequest(req); - } - -} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileCleanerCleanup.java b/java/org/apache/tomcat/util/http/fileupload/FileCleanerCleanup.java new file mode 100644 index 000000000..988a370fe --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileCleanerCleanup.java @@ -0,0 +1,81 @@ +/* + * 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.tomcat.util.http.fileupload; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletContextEvent; + + +/** + * A servlet context listener, which ensures that the + * {@link org.apache.commons.io.FileCleaner FileCleaner's} + * reaper thread is terminated, + * when the web application is destroyed. + */ +public class FileCleanerCleanup implements ServletContextListener { + /** + * Attribute name, which is used for storing an instance of + * {@link FileCleaningTracker} in the web application. + */ + public static final String FILE_CLEANING_TRACKER_ATTRIBUTE + = FileCleanerCleanup.class.getName() + ".FileCleaningTracker"; + + /** + * Returns the instance of {@link FileCleaningTracker}, which is + * associated with the given {@link ServletContext}. + * @param pServletContext The servlet context to query + * @return The contexts tracker + */ + public static FileCleaningTracker + getFileCleaningTracker(ServletContext pServletContext) { + return (FileCleaningTracker) + pServletContext.getAttribute(FILE_CLEANING_TRACKER_ATTRIBUTE); + } + + /** + * Sets the instance of {@link FileCleaningTracker}, which is + * associated with the given {@link ServletContext}. + * @param pServletContext The servlet context to modify + * @param pTracker The tracker to set + */ + public static void setFileCleaningTracker(ServletContext pServletContext, + FileCleaningTracker pTracker) { + pServletContext.setAttribute(FILE_CLEANING_TRACKER_ATTRIBUTE, pTracker); + } + + /** + * Called when the web application is initialized. Does + * nothing. + * @param sce The servlet context, used for calling + * {@link #setFileCleaningTracker(ServletContext, FileCleaningTracker)}. + */ + public void contextInitialized(ServletContextEvent sce) { + setFileCleaningTracker(sce.getServletContext(), + new FileCleaningTracker()); + } + + /** + * Called when the web application is being destroyed. + * Calls {@link FileCleaningTracker#exitWhenFinished()}. + * @param sce The servlet context, used for calling + * {@link #getFileCleaningTracker(ServletContext)}. + */ + public void contextDestroyed(ServletContextEvent sce) { + getFileCleaningTracker(sce.getServletContext()).exitWhenFinished(); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileCleaningTracker.java b/java/org/apache/tomcat/util/http/fileupload/FileCleaningTracker.java new file mode 100644 index 000000000..cd64a3262 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileCleaningTracker.java @@ -0,0 +1,258 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.Collection; +import java.util.Vector; + +/** + * Keeps track of files awaiting deletion, and deletes them when an associated + * marker object is reclaimed by the garbage collector. + *

+ * This utility creates a background thread to handle file deletion. + * Each file to be deleted is registered with a handler object. + * When the handler object is garbage collected, the file is deleted. + *

+ * In an environment with multiple class loaders (a servlet container, for + * example), you should consider stopping the background thread if it is no + * longer needed. This is done by invoking the method + * {@link #exitWhenFinished}, typically in + * {@link javax.servlet.ServletContextListener#contextDestroyed} or similar. + * + * @author Noel Bergman + * @author Martin Cooper + * @version $Id: FileCleaner.java 490987 2006-12-29 12:11:48Z scolebourne $ + */ +public class FileCleaningTracker { + /** + * Queue of Tracker instances being watched. + */ + ReferenceQueue /* Tracker */ q = new ReferenceQueue(); + /** + * Collection of Tracker instances in existence. + */ + final Collection /* Tracker */ trackers = new Vector(); // synchronized + /** + * Whether to terminate the thread when the tracking is complete. + */ + volatile boolean exitWhenFinished = false; + /** + * The thread that will clean up registered files. + */ + Thread reaper; + + //----------------------------------------------------------------------- + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the file is null + */ + public void track(File file, Object marker) { + track(file, marker, (FileDeleteStrategy) null); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the file is null + */ + public void track(File file, Object marker, FileDeleteStrategy deleteStrategy) { + if (file == null) { + throw new NullPointerException("The file must not be null"); + } + addTracker(file.getPath(), marker, deleteStrategy); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the path is null + */ + public void track(String path, Object marker) { + track(path, marker, (FileDeleteStrategy) null); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the path is null + */ + public void track(String path, Object marker, FileDeleteStrategy deleteStrategy) { + if (path == null) { + throw new NullPointerException("The path must not be null"); + } + addTracker(path, marker, deleteStrategy); + } + + /** + * Adds a tracker to the list of trackers. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + */ + private synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) { + // synchronized block protects reaper + if (exitWhenFinished) { + throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called"); + } + if (reaper == null) { + reaper = new Reaper(); + reaper.start(); + } + trackers.add(new Tracker(path, deleteStrategy, marker, q)); + } + + //----------------------------------------------------------------------- + /** + * Retrieve the number of files currently being tracked, and therefore + * awaiting deletion. + * + * @return the number of files being tracked + */ + public int getTrackCount() { + return trackers.size(); + } + + /** + * Call this method to cause the file cleaner thread to terminate when + * there are no more objects being tracked for deletion. + *

+ * In a simple environment, you don't need this method as the file cleaner + * thread will simply exit when the JVM exits. In a more complex environment, + * with multiple class loaders (such as an application server), you should be + * aware that the file cleaner thread will continue running even if the class + * loader it was started from terminates. This can consitute a memory leak. + *

+ * For example, suppose that you have developed a web application, which + * contains the commons-io jar file in your WEB-INF/lib directory. In other + * words, the FileCleaner class is loaded through the class loader of your + * web application. If the web application is terminated, but the servlet + * container is still running, then the file cleaner thread will still exist, + * posing a memory leak. + *

+ * This method allows the thread to be terminated. Simply call this method + * in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}. + * One called, no new objects can be tracked by the file cleaner. + */ + public synchronized void exitWhenFinished() { + // synchronized block protects reaper + exitWhenFinished = true; + if (reaper != null) { + synchronized (reaper) { + reaper.interrupt(); + } + } + } + + //----------------------------------------------------------------------- + /** + * The reaper thread. + */ + private final class Reaper extends Thread { + /** Construct a new Reaper */ + Reaper() { + super("File Reaper"); + setPriority(Thread.MAX_PRIORITY); + setDaemon(true); + } + + /** + * Run the reaper thread that will delete files as their associated + * marker objects are reclaimed by the garbage collector. + */ + public void run() { + // thread exits when exitWhenFinished is true and there are no more tracked objects + while (exitWhenFinished == false || trackers.size() > 0) { + Tracker tracker = null; + try { + // Wait for a tracker to remove. + tracker = (Tracker) q.remove(); + } catch (Exception e) { + continue; + } + if (tracker != null) { + tracker.delete(); + tracker.clear(); + trackers.remove(tracker); + } + } + } + } + + //----------------------------------------------------------------------- + /** + * Inner class which acts as the reference for a file pending deletion. + */ + private static final class Tracker extends PhantomReference { + + /** + * The full path to the file being tracked. + */ + private final String path; + /** + * The strategy for deleting files. + */ + private final FileDeleteStrategy deleteStrategy; + + /** + * Constructs an instance of this class from the supplied parameters. + * + * @param path the full path to the file to be tracked, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @param marker the marker object used to track the file, not null + * @param queue the queue on to which the tracker will be pushed, not null + */ + Tracker(String path, FileDeleteStrategy deleteStrategy, Object marker, ReferenceQueue queue) { + super(marker, queue); + this.path = path; + this.deleteStrategy = (deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy); + } + + /** + * Deletes the file associated with this tracker instance. + * + * @return true if the file was deleted successfully; + * false otherwise. + */ + public boolean delete() { + return deleteStrategy.deleteQuietly(new File(path)); + } + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileDeleteStrategy.java b/java/org/apache/tomcat/util/http/fileupload/FileDeleteStrategy.java new file mode 100644 index 000000000..471532204 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileDeleteStrategy.java @@ -0,0 +1,156 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.io.IOException; + +/** + * Strategy for deleting files. + *

+ * There is more than one way to delete a file. + * You may want to limit access to certain directories, to only delete + * directories if they are empty, or maybe to force deletion. + *

+ * This class captures the strategy to use and is designed for user subclassing. + * + * @author Stephen Colebourne + * @version $Id$ + * @since Commons IO 1.3 + */ +public class FileDeleteStrategy { + + /** + * The singleton instance for normal file deletion, which does not permit + * the deletion of directories that are not empty. + */ + public static final FileDeleteStrategy NORMAL = new FileDeleteStrategy("Normal"); + /** + * The singleton instance for forced file deletion, which always deletes, + * even if the file represents a non-empty directory. + */ + public static final FileDeleteStrategy FORCE = new ForceFileDeleteStrategy(); + + /** The name of the strategy. */ + private final String name; + + //----------------------------------------------------------------------- + /** + * Restricted constructor. + * + * @param name the name by which the strategy is known + */ + protected FileDeleteStrategy(String name) { + this.name = name; + } + + //----------------------------------------------------------------------- + /** + * Deletes the file object, which may be a file or a directory. + * All IOExceptions are caught and false returned instead. + * If the file does not exist or is null, true is returned. + *

+ * Subclass writers should override {@link #doDelete(File)}, not this method. + * + * @param fileToDelete the file to delete, null returns true + * @return true if the file was deleted, or there was no such file + */ + public boolean deleteQuietly(File fileToDelete) { + if (fileToDelete == null || fileToDelete.exists() == false) { + return true; + } + try { + return doDelete(fileToDelete); + } catch (IOException ex) { + return false; + } + } + + /** + * Deletes the file object, which may be a file or a directory. + * If the file does not exist, the method just returns. + *

+ * Subclass writers should override {@link #doDelete(File)}, not this method. + * + * @param fileToDelete the file to delete, not null + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + public void delete(File fileToDelete) throws IOException { + if (fileToDelete.exists() && doDelete(fileToDelete) == false) { + throw new IOException("Deletion failed: " + fileToDelete); + } + } + + /** + * Actually deletes the file object, which may be a file or a directory. + *

+ * This method is designed for subclasses to override. + * The implementation may return either false or an IOException + * when deletion fails. The {@link #delete(File)} and {@link #deleteQuietly(File)} + * methods will handle either response appropriately. + * A check has been made to ensure that the file will exist. + *

+ * This implementation uses {@link File#delete()}. + * + * @param fileToDelete the file to delete, exists, not null + * @return true if the file was deleteds + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + protected boolean doDelete(File fileToDelete) throws IOException { + return fileToDelete.delete(); + } + + //----------------------------------------------------------------------- + /** + * Gets a string describing the delete strategy. + * + * @return a string describing the delete strategy + */ + public String toString() { + return "FileDeleteStrategy[" + name + "]"; + } + + //----------------------------------------------------------------------- + /** + * Force file deletion strategy. + */ + static class ForceFileDeleteStrategy extends FileDeleteStrategy { + /** Default Constructor */ + ForceFileDeleteStrategy() { + super("Force"); + } + + /** + * Deletes the file object. + *

+ * This implementation uses FileUtils.forceDelete() + * if the file exists. + * + * @param fileToDelete the file to delete, not null + * @return Always returns true + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + protected boolean doDelete(File fileToDelete) throws IOException { + FileUtils.forceDelete(fileToDelete); + return true; + } + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItem.java b/java/org/apache/tomcat/util/http/fileupload/FileItem.java index 7237c089c..de9c9e4ee 100644 --- a/java/org/apache/tomcat/util/http/fileupload/FileItem.java +++ b/java/org/apache/tomcat/util/http/fileupload/FileItem.java @@ -5,20 +5,17 @@ * 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.tomcat.util.http.fileupload; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -26,14 +23,13 @@ import java.io.OutputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; - /** *

This class represents a file or form item that was received within a * multipart/form-data POST request. * *

After retrieving an instance of this class from a {@link - * org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} instance (see - * {@link org.apache.tomcat.util.http.fileupload.FileUpload + * org.apache.commons.fileupload.FileUpload FileUpload} instance (see + * {@link org.apache.commons.fileupload.FileUpload * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may * either request all contents of the file at once using {@link #get()} or * request an {@link java.io.InputStream InputStream} with @@ -54,9 +50,7 @@ import java.io.UnsupportedEncodingException; * * @version $Id$ */ -public interface FileItem - extends Serializable -{ +public interface FileItem extends Serializable { // ------------------------------- Methods from javax.activation.DataSource @@ -69,10 +63,9 @@ public interface FileItem * @return An {@link java.io.InputStream InputStream} that can be * used to retrieve the contents of the file. * - * @exception IOException if an error occurs. + * @throws IOException if an error occurs. */ - InputStream getInputStream() - throws IOException; + InputStream getInputStream() throws IOException; /** @@ -134,11 +127,10 @@ public interface FileItem * * @return The contents of the item, as a string. * - * @exception UnsupportedEncodingException if the requested character - * encoding is not available. + * @throws UnsupportedEncodingException if the requested character + * encoding is not available. */ - String getString(String encoding) - throws UnsupportedEncodingException; + String getString(String encoding) throws UnsupportedEncodingException; /** @@ -165,7 +157,7 @@ public interface FileItem * @param file The File into which the uploaded item should * be stored. * - * @exception Exception if an error occurs. + * @throws Exception if an error occurs. */ void write(File file) throws Exception; @@ -224,7 +216,7 @@ public interface FileItem * @return An {@link java.io.OutputStream OutputStream} that can be used * for storing the contensts of the file. * - * @exception IOException if an error occurs. + * @throws IOException if an error occurs. */ OutputStream getOutputStream() throws IOException; diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java index f6988ae24..415bab796 100644 --- a/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java @@ -5,17 +5,15 @@ * 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.tomcat.util.http.fileupload; @@ -25,11 +23,10 @@ package org.apache.tomcat.util.http.fileupload; * by the default file upload implementation.

* * @author Martin Cooper - * + * * @version $Id$ */ -public interface FileItemFactory -{ +public interface FileItemFactory { /** * Create a new {@link FileItem} instance from the supplied parameters and diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java new file mode 100644 index 000000000..7d46743d5 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.util.Iterator; + +/** + *

This class provides support for accessing the headers for a file or form + * item that was received within a multipart/form-data POST + * request.

+ * + * @author Michael C. Macaluso + * @since 1.3 + */ +public interface FileItemHeaders { + /** + * Returns the value of the specified part header as a String. + * If the part did not include a header of the specified name, this method + * return null. If there are multiple headers with the same + * name, this method returns the first header in the item. The header + * name is case insensitive. + * + * @param name a String specifying the header name + * @return a String containing the value of the requested + * header, or null if the item does not have a header + * of that name + */ + String getHeader(String name); + + /** + *

+ * Returns all the values of the specified item header as an + * Enumeration of String objects. + *

+ *

+ * If the item did not include any headers of the specified name, this + * method returns an empty Enumeration. The header name is + * case insensitive. + *

+ * + * @param name a String specifying the header name + * @return an Enumeration containing the values of the + * requested header. If the item does not have any headers of + * that name, return an empty Enumeration + */ + Iterator getHeaders(String name); + + /** + *

+ * Returns an Enumeration of all the header names. + *

+ *

+ * If the item did not include any headers of the specified name, this + * method returns an empty Enumeration. The header name is + * case insensitive. + *

+ * + * @return an Enumeration containing the values of the + * requested header. If the item does not have any headers of + * that name return an empty Enumeration + */ + Iterator getHeaderNames(); +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersImpl.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersImpl.java new file mode 100644 index 000000000..0c6ee19d3 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersImpl.java @@ -0,0 +1,88 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * Default implementation of the {@link FileItemHeaders} interface. + * + * @author Michael C. Macaluso + * @since 1.3 + */ +public class FileItemHeadersImpl implements FileItemHeaders, Serializable { + private static final long serialVersionUID = -4455695752627032559L; + + /** + * Map of String keys to a List of + * String instances. + */ + private final Map headerNameToValueListMap = new HashMap(); + + /** + * List to preserve order of headers as added. This would not be + * needed if a LinkedHashMap could be used, but don't + * want to depend on 1.4. + */ + private final List headerNameList = new ArrayList(); + + public String getHeader(String name) { + String nameLower = name.toLowerCase(); + List headerValueList = (List) headerNameToValueListMap.get(nameLower); + if (null == headerValueList) { + return null; + } + return (String) headerValueList.get(0); + } + + public Iterator getHeaderNames() { + return headerNameList.iterator(); + } + + public Iterator getHeaders(String name) { + String nameLower = name.toLowerCase(); + List headerValueList = (List) headerNameToValueListMap.get(nameLower); + if (null == headerValueList) { + return Collections.EMPTY_LIST.iterator(); + } + return headerValueList.iterator(); + } + + /** + * Method to add header values to this instance. + * + * @param name name of this header + * @param value value of this header + */ + public synchronized void addHeader(String name, String value) { + String nameLower = name.toLowerCase(); + List headerValueList = (List) headerNameToValueListMap.get(nameLower); + if (null == headerValueList) { + headerValueList = new ArrayList(); + headerNameToValueListMap.put(nameLower, headerValueList); + headerNameList.add(nameLower); + } + headerValueList.add(value); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java new file mode 100644 index 000000000..b478d12da --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java @@ -0,0 +1,47 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + * Interface that will indicate that {@link FileItem} or {@link FileItemStream} + * implementations will accept the headers read for the item. + * + * @author Michael C. Macaluso + * @since 1.3 + * + * @see FileItem + * @see FileItemStream + */ +public interface FileItemHeadersSupport { + /** + * Returns the collection of headers defined locally within this item. + * + * @return the {@link FileItemHeaders} present for this item. + */ + FileItemHeaders getHeaders(); + + /** + * Sets the headers read from within an item. Implementations of + * {@link FileItem} or {@link FileItemStream} should implement this + * interface to be able to get the raw headers found within the item + * header block. + * + * @param headers the instance that holds onto the headers + * for this instance. + */ + void setHeaders(FileItemHeaders headers); +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java b/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java new file mode 100644 index 000000000..7279c7500 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.util.http.fileupload; +import java.io.IOException; + + +/** + * An iterator, as returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}. + */ +public interface FileItemIterator { + /** + * Returns, whether another instance of {@link FileItemStream} + * is available. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return True, if one or more additional file items + * are available, otherwise false. + */ + boolean hasNext() throws FileUploadException, IOException; + + /** + * Returns the next available {@link FileItemStream}. + * @throws java.util.NoSuchElementException No more items are available. Use + * {@link #hasNext()} to prevent this exception. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return FileItemStream instance, which provides + * access to the next file item. + */ + FileItemStream next() throws FileUploadException, IOException; +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java new file mode 100644 index 000000000..92f94874d --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java @@ -0,0 +1,97 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.InputStream; + + +/** + *

This interface provides access to a file or form item that was + * received within a multipart/form-data POST request. + * The items contents are retrieved by calling {@link #openStream()}.

+ *

Instances of this class are created by accessing the + * iterator, returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}.

+ *

Note: There is an interaction between the iterator and + * its associated instances of {@link FileItemStream}: By invoking + * {@link java.util.Iterator#hasNext()} on the iterator, you discard all data, + * which hasn't been read so far from the previous data.

+ */ +public interface FileItemStream extends FileItemHeadersSupport { + /** + * This exception is thrown, if an attempt is made to read + * data from the {@link InputStream}, which has been returned + * by {@link FileItemStream#openStream()}, after + * {@link java.util.Iterator#hasNext()} has been invoked on the + * iterator, which created the {@link FileItemStream}. + */ + public static class ItemSkippedException extends IOException { + /** + * The exceptions serial version UID, which is being used + * when serializing an exception instance. + */ + private static final long serialVersionUID = -7280778431581963740L; + } + + /** Creates an {@link InputStream}, which allows to read the + * items contents. + * @return The input stream, from which the items data may + * be read. + * @throws IllegalStateException The method was already invoked on + * this item. It is not possible to recreate the data stream. + * @throws IOException An I/O error occurred. + * @see ItemSkippedException + */ + InputStream openStream() throws IOException; + + /** + * Returns the content type passed by the browser or null if + * not defined. + * + * @return The content type passed by the browser or null if + * not defined. + */ + String getContentType(); + + /** + * Returns the original filename in the client's filesystem, as provided by + * the browser (or other client software). In most cases, this will be the + * base file name, without path information. However, some clients, such as + * the Opera browser, do include path information. + * + * @return The original filename in the client's filesystem. + */ + String getName(); + + /** + * Returns the name of the field in the multipart form corresponding to + * this file item. + * + * @return The name of the form field. + */ + String getFieldName(); + + /** + * Determines whether or not a FileItem instance represents + * a simple form field. + * + * @return true if the instance represents a simple form + * field; false if it represents an uploaded file. + */ + boolean isFormField(); +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java index a417cb31d..867627307 100644 --- a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java +++ b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java @@ -5,17 +5,15 @@ * 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.tomcat.util.http.fileupload; @@ -25,9 +23,9 @@ package org.apache.tomcat.util.http.fileupload; *

This class handles multiple files per single HTML widget, sent using * multipart/mixed encoding type, as specified by * RFC 1867. Use {@link - * #parseRequest(HttpServletRequest)} to acquire a list of {@link - * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML - * widget.

+ * #parseRequest(javax.servlet.http.HttpServletRequest)} to acquire a list + * of {@link org.apache.commons.fileupload.FileItem FileItems} associated + * with a given HTML widget.

* *

How the data for individual parts is stored is determined by the factory * used to create them; a given part may be in memory, on disk, or somewhere @@ -43,8 +41,7 @@ package org.apache.tomcat.util.http.fileupload; * @version $Id$ */ public class FileUpload - extends FileUploadBase - { + extends FileUploadBase { // ----------------------------------------------------------- Data members @@ -59,13 +56,13 @@ public class FileUpload /** - * Constructs an instance of this class which uses the default factory to - * create FileItem instances. + * Constructs an uninitialised instance of this class. A factory must be + * configured, using setFileItemFactory(), before attempting + * to parse requests. * * @see #FileUpload(FileItemFactory) */ - public FileUpload() - { + public FileUpload() { super(); } @@ -75,9 +72,9 @@ public class FileUpload * create FileItem instances. * * @see #FileUpload() + * @param fileItemFactory The factory to use for creating file items. */ - public FileUpload(FileItemFactory fileItemFactory) - { + public FileUpload(FileItemFactory fileItemFactory) { super(); this.fileItemFactory = fileItemFactory; } @@ -91,9 +88,7 @@ public class FileUpload * * @return The factory class for new file items. */ - @Override - public FileItemFactory getFileItemFactory() - { + public FileItemFactory getFileItemFactory() { return fileItemFactory; } @@ -103,9 +98,7 @@ public class FileUpload * * @param factory The factory class for new file items. */ - @Override - public void setFileItemFactory(FileItemFactory factory) - { + public void setFileItemFactory(FileItemFactory factory) { this.fileItemFactory = factory; } diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java index efced3202..341d18277 100644 --- a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java @@ -5,29 +5,31 @@ * 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.tomcat.util.http.fileupload; - import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; + import javax.servlet.http.HttpServletRequest; +import org.apache.tomcat.util.http.fileupload.MultipartStream.ItemInputStream; + /** *

High level API for processing file uploads.

@@ -36,7 +38,7 @@ import javax.servlet.http.HttpServletRequest; * multipart/mixed encoding type, as specified by * RFC 1867. Use {@link * #parseRequest(HttpServletRequest)} to acquire a list of {@link - * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML + * org.apache.commons.fileupload.FileItem}s associated with a given HTML * widget.

* *

How the data for individual parts is stored is determined by the factory @@ -52,36 +54,53 @@ import javax.servlet.http.HttpServletRequest; * * @version $Id$ */ -public abstract class FileUploadBase -{ +public abstract class FileUploadBase { // ---------------------------------------------------------- Class methods /** - * Utility method that determines whether the request contains multipart - * content. + *

Utility method that determines whether the request contains multipart + * content.

* - * @param req The servlet request to be evaluated. Must be non-null. + *

NOTE:This method will be moved to the + * ServletFileUpload class after the FileUpload 1.1 release. + * Unfortunately, since this method is static, it is not possible to + * provide its replacement until this method is removed.

+ * + * @param ctx The request context to be evaluated. Must be non-null. * * @return true if the request is multipart; * false otherwise. */ - public static final boolean isMultipartContent(HttpServletRequest req) - { - String contentType = req.getHeader(CONTENT_TYPE); - if (contentType == null) - { + public static final boolean isMultipartContent(RequestContext ctx) { + String contentType = ctx.getContentType(); + if (contentType == null) { return false; } - if (contentType.startsWith(MULTIPART)) - { + if (contentType.toLowerCase().startsWith(MULTIPART)) { return true; } return false; } + /** + * Utility method that determines whether the request contains multipart + * content. + * + * @param req The servlet request to be evaluated. Must be non-null. + * + * @return true if the request is multipart; + * false otherwise. + * + * @deprecated Use the method on ServletFileUpload instead. + */ + public static boolean isMultipartContent(HttpServletRequest req) { + return ServletFileUpload.isMultipartContent(req); + } + + // ----------------------------------------------------- Manifest constants @@ -96,6 +115,11 @@ public abstract class FileUploadBase */ public static final String CONTENT_DISPOSITION = "Content-disposition"; + /** + * HTTP content length header name. + */ + public static final String CONTENT_LENGTH = "Content-length"; + /** * Content-disposition value for form data. @@ -130,6 +154,9 @@ public abstract class FileUploadBase /** * The maximum length of a single header line that will be parsed * (1024 bytes). + * @deprecated This constant is no longer used. As of commons-fileupload + * 1.2, the only applicable limit is the total size of a parts headers, + * {@link MultipartStream#HEADER_PART_SIZE_MAX}. */ public static final int MAX_HEADER_SIZE = 1024; @@ -138,17 +165,26 @@ public abstract class FileUploadBase /** - * The maximum size permitted for an uploaded file. A value of -1 indicates - * no maximum. + * The maximum size permitted for the complete request, as opposed to + * {@link #fileSizeMax}. A value of -1 indicates no maximum. */ private long sizeMax = -1; + /** + * The maximum size permitted for a single uploaded file, as opposed + * to {@link #sizeMax}. A value of -1 indicates no maximum. + */ + private long fileSizeMax = -1; /** * The content encoding to use when reading part headers. */ private String headerEncoding; + /** + * The progress listener. + */ + private ProgressListener listener; // ----------------------------------------------------- Property accessors @@ -170,55 +206,78 @@ public abstract class FileUploadBase /** - * Returns the maximum allowed upload size. + * Returns the maximum allowed size of a complete request, as opposed + * to {@link #getFileSizeMax()}. * - * @return The maximum allowed size, in bytes. + * @return The maximum allowed size, in bytes. The default value of + * -1 indicates, that there is no limit. * * @see #setSizeMax(long) * */ - public long getSizeMax() - { + public long getSizeMax() { return sizeMax; } /** - * Sets the maximum allowed upload size. If negative, there is no maximum. + * Sets the maximum allowed size of a complete request, as opposed + * to {@link #setFileSizeMax(long)}. * - * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum. + * @param sizeMax The maximum allowed size, in bytes. The default value of + * -1 indicates, that there is no limit. * * @see #getSizeMax() * */ - public void setSizeMax(long sizeMax) - { + public void setSizeMax(long sizeMax) { this.sizeMax = sizeMax; } + /** + * Returns the maximum allowed size of a single uploaded file, + * as opposed to {@link #getSizeMax()}. + * + * @see #setFileSizeMax(long) + * @return Maximum size of a single uploaded file. + */ + public long getFileSizeMax() { + return fileSizeMax; + } + + /** + * Sets the maximum allowed size of a single uploaded file, + * as opposed to {@link #getSizeMax()}. + * + * @see #getFileSizeMax() + * @param fileSizeMax Maximum size of a single uploaded file. + */ + public void setFileSizeMax(long fileSizeMax) { + this.fileSizeMax = fileSizeMax; + } /** * Retrieves the character encoding used when reading the headers of an - * individual part. When not specified, or null, the platform - * default encoding is used. + * individual part. When not specified, or null, the request + * encoding is used. If that is also not specified, or null, + * the platform default encoding is used. * * @return The encoding used to read part headers. */ - public String getHeaderEncoding() - { + public String getHeaderEncoding() { return headerEncoding; } /** * Specifies the character encoding to be used when reading the headers of - * individual parts. When not specified, or null, the platform - * default encoding is used. + * individual part. When not specified, or null, the request + * encoding is used. If that is also not specified, or null, + * the platform default encoding is used. * * @param encoding The encoding used to read part headers. */ - public void setHeaderEncoding(String encoding) - { + public void setHeaderEncoding(String encoding) { headerEncoding = encoding; } @@ -228,166 +287,93 @@ public abstract class FileUploadBase /** * Processes an RFC 1867 - * compliant multipart/form-data stream. If files are stored - * on disk, the path is given by getRepository(). + * compliant multipart/form-data stream. * * @param req The servlet request to be parsed. * * @return A list of FileItem instances parsed from the * request, in the order that they were transmitted. * - * @exception FileUploadException if there are problems reading/parsing - * the request or storing files. + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + * + * @deprecated Use the method in ServletFileUpload instead. */ - public List parseRequest(HttpServletRequest req) - throws FileUploadException - { - if (null == req) - { - throw new NullPointerException("req parameter"); - } - - ArrayList items = new ArrayList(); - String contentType = req.getHeader(CONTENT_TYPE); - - if ((null == contentType) || (!contentType.startsWith(MULTIPART))) - { - throw new InvalidContentTypeException( - "the request doesn't contain a " - + MULTIPART_FORM_DATA - + " or " - + MULTIPART_MIXED - + " stream, content type header is " - + contentType); - } - int requestSize = req.getContentLength(); - - if (requestSize == -1) - { - throw new UnknownSizeException( - "the request was rejected because it's size is unknown"); - } + public List /* FileItem */ parseRequest(HttpServletRequest req) + throws FileUploadException { + return parseRequest(new ServletRequestContext(req)); + } - if (sizeMax >= 0 && requestSize > sizeMax) - { - throw new SizeLimitExceededException( - "the request was rejected because " - + "it's size exceeds allowed range"); - } + /** + * Processes an RFC 1867 + * compliant multipart/form-data stream. + * + * @param ctx The context for the request to be parsed. + * + * @return An iterator to instances of FileItemStream + * parsed from the request, in the order that they were + * transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + * @throws IOException An I/O error occurred. This may be a network + * error while communicating with the client or a problem while + * storing the uploaded content. + */ + public FileItemIterator getItemIterator(RequestContext ctx) + throws FileUploadException, IOException { + return new FileItemIteratorImpl(ctx); + } - try - { - int boundaryIndex = contentType.indexOf("boundary="); - if (boundaryIndex < 0) - { - throw new FileUploadException( - "the request was rejected because " - + "no multipart boundary was found"); + /** + * Processes an RFC 1867 + * compliant multipart/form-data stream. + * + * @param ctx The context for the request to be parsed. + * + * @return A list of FileItem instances parsed from the + * request, in the order that they were transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + */ + public List /* FileItem */ parseRequest(RequestContext ctx) + throws FileUploadException { + try { + FileItemIterator iter = getItemIterator(ctx); + List items = new ArrayList(); + FileItemFactory fac = getFileItemFactory(); + if (fac == null) { + throw new NullPointerException( + "No FileItemFactory has been set."); } - byte[] boundary = contentType.substring( - boundaryIndex + 9).getBytes(); - - InputStream input = req.getInputStream(); - - MultipartStream multi = new MultipartStream(input, boundary); - multi.setHeaderEncoding(headerEncoding); - - boolean nextPart = multi.skipPreamble(); - while (nextPart) - { - Map headers = parseHeaders(multi.readHeaders()); - String fieldName = getFieldName(headers); - if (fieldName != null) - { - String subContentType = getHeader(headers, CONTENT_TYPE); - if (subContentType != null && subContentType - .startsWith(MULTIPART_MIXED)) - { - // Multiple files. - byte[] subBoundary = - subContentType.substring( - subContentType - .indexOf("boundary=") + 9).getBytes(); - multi.setBoundary(subBoundary); - boolean nextSubPart = multi.skipPreamble(); - while (nextSubPart) - { - headers = parseHeaders(multi.readHeaders()); - if (getFileName(headers) != null) - { - FileItem item = - createItem(headers, false); - OutputStream os = item.getOutputStream(); - try - { - multi.readBodyData(os); - } - finally - { - os.close(); - } - items.add(item); - } - else - { - // Ignore anything but files inside - // multipart/mixed. - multi.discardBodyData(); - } - nextSubPart = multi.readBoundary(); - } - multi.setBoundary(boundary); - } - else - { - if (getFileName(headers) != null) - { - // A single file. - FileItem item = createItem(headers, false); - OutputStream os = item.getOutputStream(); - try - { - multi.readBodyData(os); - } - finally - { - os.close(); - } - items.add(item); - } - else - { - // A form field. - FileItem item = createItem(headers, true); - OutputStream os = item.getOutputStream(); - try - { - multi.readBodyData(os); - } - finally - { - os.close(); - } - items.add(item); - } - } + while (iter.hasNext()) { + FileItemStream item = iter.next(); + FileItem fileItem = fac.createItem(item.getFieldName(), + item.getContentType(), item.isFormField(), + item.getName()); + try { + Streams.copy(item.openStream(), fileItem.getOutputStream(), + true); + } catch (FileUploadIOException e) { + throw (FileUploadException) e.getCause(); + } catch (IOException e) { + throw new IOFileUploadException( + "Processing of " + MULTIPART_FORM_DATA + + " request failed. " + e.getMessage(), e); } - else - { - // Skip this part. - multi.discardBodyData(); + if (fileItem instanceof FileItemHeadersSupport) { + final FileItemHeaders fih = item.getHeaders(); + ((FileItemHeadersSupport) fileItem).setHeaders(fih); } - nextPart = multi.readBoundary(); + items.add(fileItem); } + return items; + } catch (FileUploadIOException e) { + throw (FileUploadException) e.getCause(); + } catch (IOException e) { + throw new FileUploadException(e.getMessage(), e); } - catch (IOException e) - { - throw new FileUploadException( - "Processing of " + MULTIPART_FORM_DATA - + " request failed. " + e.getMessage()); - } - - return items; } @@ -395,24 +381,83 @@ public abstract class FileUploadBase /** + * Retrieves the boundary from the Content-type header. + * + * @param contentType The value of the content type header from which to + * extract the boundary value. + * + * @return The boundary, as a byte array. + */ + protected byte[] getBoundary(String contentType) { + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = parser.parse(contentType, new char[] {';', ','}); + String boundaryStr = (String) params.get("boundary"); + + if (boundaryStr == null) { + return null; + } + byte[] boundary; + try { + boundary = boundaryStr.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + boundary = boundaryStr.getBytes(); + } + return boundary; + } + + + /** * Retrieves the file name from the Content-disposition * header. * * @param headers A Map containing the HTTP request headers. * * @return The file name for the current encapsulation. + * @deprecated Use {@link #getFileName(FileItemHeaders)}. */ - protected String getFileName(Map headers) - { + protected String getFileName(Map /* String, String */ headers) { + return getFileName(getHeader(headers, CONTENT_DISPOSITION)); + } + + /** + * Retrieves the file name from the Content-disposition + * header. + * + * @param headers The HTTP headers object. + * + * @return The file name for the current encapsulation. + */ + protected String getFileName(FileItemHeaders headers) { + return getFileName(headers.getHeader(CONTENT_DISPOSITION)); + } + + /** + * Returns the given content-disposition headers file name. + * @param pContentDisposition The content-disposition headers value. + * @return The file name + */ + private String getFileName(String pContentDisposition) { String fileName = null; - String cd = getHeader(headers, CONTENT_DISPOSITION); - if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT)) - { - int start = cd.indexOf("filename=\""); - int end = cd.indexOf('"', start + 10); - if (start != -1 && end != -1) - { - fileName = cd.substring(start + 10, end).trim(); + if (pContentDisposition != null) { + String cdl = pContentDisposition.toLowerCase(); + if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) { + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = parser.parse(pContentDisposition, ';'); + if (params.containsKey("filename")) { + fileName = (String) params.get("filename"); + if (fileName != null) { + fileName = fileName.trim(); + } else { + // Even if there is no value, the parameter is present, + // so we return an empty file name rather than no file + // name. + fileName = ""; + } + } } } return fileName; @@ -427,22 +472,45 @@ public abstract class FileUploadBase * * @return The field name for the current encapsulation. */ - protected String getFieldName(Map headers) - { + protected String getFieldName(FileItemHeaders headers) { + return getFieldName(headers.getHeader(CONTENT_DISPOSITION)); + } + + /** + * Returns the field name, which is given by the content-disposition + * header. + * @param pContentDisposition The content-dispositions header value. + * @return The field jake + */ + private String getFieldName(String pContentDisposition) { String fieldName = null; - String cd = getHeader(headers, CONTENT_DISPOSITION); - if (cd != null && cd.startsWith(FORM_DATA)) - { - int start = cd.indexOf("name=\""); - int end = cd.indexOf('"', start + 6); - if (start != -1 && end != -1) - { - fieldName = cd.substring(start + 6, end); + if (pContentDisposition != null + && pContentDisposition.toLowerCase().startsWith(FORM_DATA)) { + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = parser.parse(pContentDisposition, ';'); + fieldName = (String) params.get("name"); + if (fieldName != null) { + fieldName = fieldName.trim(); } } return fieldName; } + /** + * Retrieves the field name from the Content-disposition + * header. + * + * @param headers A Map containing the HTTP request headers. + * + * @return The field name for the current encapsulation. + * @deprecated Use {@link #getFieldName(FileItemHeaders)}. + */ + protected String getFieldName(Map /* String, String */ headers) { + return getFieldName(getHeader(headers, CONTENT_DISPOSITION)); + } + /** * Creates a new {@link FileItem} instance. @@ -454,19 +522,19 @@ public abstract class FileUploadBase * * @return A newly created FileItem instance. * - * @exception FileUploadException if an error occurs. + * @throws FileUploadException if an error occurs. + * @deprecated This method is no longer used in favour of + * internally created instances of {@link FileItem}. */ - protected FileItem createItem(Map headers, + protected FileItem createItem(Map /* String, String */ headers, boolean isFormField) - throws FileUploadException - { + throws FileUploadException { return getFileItemFactory().createItem(getFieldName(headers), getHeader(headers, CONTENT_TYPE), isFormField, getFileName(headers)); } - /** *

Parses the header-part and returns as key/value * pairs. @@ -479,64 +547,114 @@ public abstract class FileUploadBase * * @return A Map containing the parsed HTTP request headers. */ - protected Map parseHeaders(String headerPart) - { - Map headers = new HashMap(); - char buffer[] = new char[MAX_HEADER_SIZE]; - boolean done = false; - int j = 0; - int i; - String header, headerName, headerValue; - try - { - while (!done) - { - i = 0; - // Copy a single line of characters into the buffer, - // omitting trailing CRLF. - while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n') - { - buffer[i++] = headerPart.charAt(j++); - } - header = new String(buffer, 0, i - 2); - if (header.equals("")) - { - done = true; - } - else - { - if (header.indexOf(':') == -1) - { - // This header line is malformed, skip it. - continue; - } - headerName = header.substring(0, header.indexOf(':')) - .trim().toLowerCase(); - headerValue = - header.substring(header.indexOf(':') + 1).trim(); - if (getHeader(headers, headerName) != null) - { - // More that one heder of that name exists, - // append to the list. - headers.put(headerName, - getHeader(headers, headerName) + ',' - + headerValue); - } - else - { - headers.put(headerName, headerValue); + protected FileItemHeaders getParsedHeaders(String headerPart) { + final int len = headerPart.length(); + FileItemHeadersImpl headers = newFileItemHeaders(); + int start = 0; + for (;;) { + int end = parseEndOfLine(headerPart, start); + if (start == end) { + break; + } + String header = headerPart.substring(start, end); + start = end + 2; + while (start < len) { + int nonWs = start; + while (nonWs < len) { + char c = headerPart.charAt(nonWs); + if (c != ' ' && c != '\t') { + break; } + ++nonWs; + } + if (nonWs == start) { + break; } + // Continuation line found + end = parseEndOfLine(headerPart, nonWs); + header += " " + headerPart.substring(nonWs, end); + start = end + 2; } - } - catch (IndexOutOfBoundsException e) - { - // Headers were malformed. continue with all that was - // parsed. + parseHeaderLine(headers, header); } return headers; } + /** + * Creates a new instance of {@link FileItemHeaders}. + * @return The new instance. + */ + protected FileItemHeadersImpl newFileItemHeaders() { + return new FileItemHeadersImpl(); + } + + /** + *

Parses the header-part and returns as key/value + * pairs. + * + *

If there are multiple headers of the same names, the name + * will map to a comma-separated list containing the values. + * + * @param headerPart The header-part of the current + * encapsulation. + * + * @return A Map containing the parsed HTTP request headers. + * @deprecated Use {@link #getParsedHeaders(String)} + */ + protected Map /* String, String */ parseHeaders(String headerPart) { + FileItemHeaders headers = getParsedHeaders(headerPart); + Map result = new HashMap(); + for (Iterator iter = headers.getHeaderNames(); iter.hasNext();) { + String headerName = (String) iter.next(); + Iterator iter2 = headers.getHeaders(headerName); + String headerValue = (String) iter2.next(); + while (iter2.hasNext()) { + headerValue += "," + iter2.next(); + } + result.put(headerName, headerValue); + } + return result; + } + + /** + * Skips bytes until the end of the current line. + * @param headerPart The headers, which are being parsed. + * @param end Index of the last byte, which has yet been + * processed. + * @return Index of the \r\n sequence, which indicates + * end of line. + */ + private int parseEndOfLine(String headerPart, int end) { + int index = end; + for (;;) { + int offset = headerPart.indexOf('\r', index); + if (offset == -1 || offset + 1 >= headerPart.length()) { + throw new IllegalStateException( + "Expected headers to be terminated by an empty line."); + } + if (headerPart.charAt(offset + 1) == '\n') { + return offset; + } + index = offset + 1; + } + } + + /** + * Reads the next header line. + * @param headers String with all headers. + * @param header Map where to store the current header. + */ + private void parseHeaderLine(FileItemHeadersImpl headers, String header) { + final int colonOffset = header.indexOf(':'); + if (colonOffset == -1) { + // This header line is malformed, skip it. + return; + } + String headerName = header.substring(0, colonOffset).trim(); + String headerValue = + header.substring(header.indexOf(':') + 1).trim(); + headers.addHeader(headerName, headerValue); + } /** * Returns the header with the specified name from the supplied map. The @@ -547,27 +665,442 @@ public abstract class FileUploadBase * * @return The value of specified header, or a comma-separated list if * there were multiple headers of that name. + * @deprecated Use {@link FileItemHeaders#getHeader(String)}. */ - protected final String getHeader(Map headers, - String name) - { - return headers.get(name.toLowerCase()); + protected final String getHeader(Map /* String, String */ headers, + String name) { + return (String) headers.get(name.toLowerCase()); } + /** + * The iterator, which is returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}. + */ + private class FileItemIteratorImpl implements FileItemIterator { + /** + * Default implementation of {@link FileItemStream}. + */ + private class FileItemStreamImpl implements FileItemStream { + /** The file items content type. + */ + private final String contentType; + /** The file items field name. + */ + private final String fieldName; + /** The file items file name. + */ + private final String name; + /** Whether the file item is a form field. + */ + private final boolean formField; + /** The file items input stream. + */ + private final InputStream stream; + /** Whether the file item was already opened. + */ + private boolean opened; + /** The headers, if any. + */ + private FileItemHeaders headers; + + /** + * Creates a new instance. + * @param pName The items file name, or null. + * @param pFieldName The items field name. + * @param pContentType The items content type, or null. + * @param pFormField Whether the item is a form field. + * @param pContentLength The items content length, if known, or -1 + * @throws IOException Creating the file item failed. + */ + FileItemStreamImpl(String pName, String pFieldName, + String pContentType, boolean pFormField, + long pContentLength) throws IOException { + name = pName; + fieldName = pFieldName; + contentType = pContentType; + formField = pFormField; + final ItemInputStream itemStream = multi.newInputStream(); + InputStream istream = itemStream; + if (fileSizeMax != -1) { + if (pContentLength != -1 + && pContentLength > fileSizeMax) { + FileUploadException e = + new FileSizeLimitExceededException( + "The field " + fieldName + + " exceeds its maximum permitted " + + " size of " + fileSizeMax + + " characters.", + pContentLength, fileSizeMax); + throw new FileUploadIOException(e); + } + istream = new LimitedInputStream(istream, fileSizeMax) { + protected void raiseError(long pSizeMax, long pCount) + throws IOException { + itemStream.close(true); + FileUploadException e = + new FileSizeLimitExceededException( + "The field " + fieldName + + " exceeds its maximum permitted " + + " size of " + pSizeMax + + " characters.", + pCount, pSizeMax); + throw new FileUploadIOException(e); + } + }; + } + stream = istream; + } + + /** + * Returns the items content type, or null. + * @return Content type, if known, or null. + */ + public String getContentType() { + return contentType; + } + + /** + * Returns the items field name. + * @return Field name. + */ + public String getFieldName() { + return fieldName; + } + + /** + * Returns the items file name. + * @return File name, if known, or null. + */ + public String getName() { + return name; + } + + /** + * Returns, whether this is a form field. + * @return True, if the item is a form field, + * otherwise false. + */ + public boolean isFormField() { + return formField; + } + + /** + * Returns an input stream, which may be used to + * read the items contents. + * @return Opened input stream. + * @throws IOException An I/O error occurred. + */ + public InputStream openStream() throws IOException { + if (opened) { + throw new IllegalStateException( + "The stream was already opened."); + } + if (((Closeable) stream).isClosed()) { + throw new FileItemStream.ItemSkippedException(); + } + return stream; + } + + /** + * Closes the file item. + * @throws IOException An I/O error occurred. + */ + void close() throws IOException { + stream.close(); + } + + /** + * Returns the file item headers. + * @return The items header object + */ + public FileItemHeaders getHeaders() { + return headers; + } + + /** + * Sets the file item headers. + * @param pHeaders The items header object + */ + public void setHeaders(FileItemHeaders pHeaders) { + headers = pHeaders; + } + } + + /** + * The multi part stream to process. + */ + private final MultipartStream multi; + /** + * The notifier, which used for triggering the + * {@link ProgressListener}. + */ + private final MultipartStream.ProgressNotifier notifier; + /** + * The boundary, which separates the various parts. + */ + private final byte[] boundary; + /** + * The item, which we currently process. + */ + private FileItemStreamImpl currentItem; + /** + * The current items field name. + */ + private String currentFieldName; + /** + * Whether we are currently skipping the preamble. + */ + private boolean skipPreamble; + /** + * Whether the current item may still be read. + */ + private boolean itemValid; + /** + * Whether we have seen the end of the file. + */ + private boolean eof; + + /** + * Creates a new instance. + * @param ctx The request context. + * @throws FileUploadException An error occurred while + * parsing the request. + * @throws IOException An I/O error occurred. + */ + FileItemIteratorImpl(RequestContext ctx) + throws FileUploadException, IOException { + if (ctx == null) { + throw new NullPointerException("ctx parameter"); + } + + String contentType = ctx.getContentType(); + if ((null == contentType) + || (!contentType.toLowerCase().startsWith(MULTIPART))) { + throw new InvalidContentTypeException( + "the request doesn't contain a " + + MULTIPART_FORM_DATA + + " or " + + MULTIPART_MIXED + + " stream, content type header is " + + contentType); + } + + InputStream input = ctx.getInputStream(); + + if (sizeMax >= 0) { + int requestSize = ctx.getContentLength(); + if (requestSize == -1) { + input = new LimitedInputStream(input, sizeMax) { + protected void raiseError(long pSizeMax, long pCount) + throws IOException { + FileUploadException ex = + new SizeLimitExceededException( + "the request was rejected because" + + " its size (" + pCount + + ") exceeds the configured maximum" + + " (" + pSizeMax + ")", + pCount, pSizeMax); + throw new FileUploadIOException(ex); + } + }; + } else { + if (sizeMax >= 0 && requestSize > sizeMax) { + throw new SizeLimitExceededException( + "the request was rejected because its size (" + + requestSize + + ") exceeds the configured maximum (" + + sizeMax + ")", + requestSize, sizeMax); + } + } + } + + String charEncoding = headerEncoding; + if (charEncoding == null) { + charEncoding = ctx.getCharacterEncoding(); + } + + boundary = getBoundary(contentType); + if (boundary == null) { + throw new FileUploadException( + "the request was rejected because " + + "no multipart boundary was found"); + } + + notifier = new MultipartStream.ProgressNotifier(listener, + ctx.getContentLength()); + multi = new MultipartStream(input, boundary, notifier); + multi.setHeaderEncoding(charEncoding); + + skipPreamble = true; + findNextItem(); + } + + /** + * Called for finding the nex item, if any. + * @return True, if an next item was found, otherwise false. + * @throws IOException An I/O error occurred. + */ + private boolean findNextItem() throws IOException { + if (eof) { + return false; + } + if (currentItem != null) { + currentItem.close(); + currentItem = null; + } + for (;;) { + boolean nextPart; + if (skipPreamble) { + nextPart = multi.skipPreamble(); + } else { + nextPart = multi.readBoundary(); + } + if (!nextPart) { + if (currentFieldName == null) { + // Outer multipart terminated -> No more data + eof = true; + return false; + } + // Inner multipart terminated -> Return to parsing the outer + multi.setBoundary(boundary); + currentFieldName = null; + continue; + } + FileItemHeaders headers = getParsedHeaders(multi.readHeaders()); + if (currentFieldName == null) { + // We're parsing the outer multipart + String fieldName = getFieldName(headers); + if (fieldName != null) { + String subContentType = headers.getHeader(CONTENT_TYPE); + if (subContentType != null + && subContentType.toLowerCase() + .startsWith(MULTIPART_MIXED)) { + currentFieldName = fieldName; + // Multiple files associated with this field name + byte[] subBoundary = getBoundary(subContentType); + multi.setBoundary(subBoundary); + skipPreamble = true; + continue; + } + String fileName = getFileName(headers); + currentItem = new FileItemStreamImpl(fileName, + fieldName, headers.getHeader(CONTENT_TYPE), + fileName == null, getContentLength(headers)); + notifier.noteItem(); + itemValid = true; + return true; + } + } else { + String fileName = getFileName(headers); + if (fileName != null) { + currentItem = new FileItemStreamImpl(fileName, + currentFieldName, + headers.getHeader(CONTENT_TYPE), + false, getContentLength(headers)); + notifier.noteItem(); + itemValid = true; + return true; + } + } + multi.discardBodyData(); + } + } + + private long getContentLength(FileItemHeaders pHeaders) { + try { + return Long.parseLong(pHeaders.getHeader(CONTENT_LENGTH)); + } catch (Exception e) { + return -1; + } + } + + /** + * Returns, whether another instance of {@link FileItemStream} + * is available. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return True, if one or more additional file items + * are available, otherwise false. + */ + public boolean hasNext() throws FileUploadException, IOException { + if (eof) { + return false; + } + if (itemValid) { + return true; + } + return findNextItem(); + } + + /** + * Returns the next available {@link FileItemStream}. + * @throws java.util.NoSuchElementException No more items are + * available. Use {@link #hasNext()} to prevent this exception. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return FileItemStream instance, which provides + * access to the next file item. + */ + public FileItemStream next() throws FileUploadException, IOException { + if (eof || (!itemValid && !hasNext())) { + throw new NoSuchElementException(); + } + itemValid = false; + return currentItem; + } + } + + /** + * This exception is thrown for hiding an inner + * {@link FileUploadException} in an {@link IOException}. + */ + public static class FileUploadIOException extends IOException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -7047616958165584154L; + /** The exceptions cause; we overwrite the parent + * classes field, which is available since Java + * 1.4 only. + */ + private final FileUploadException cause; + + /** + * Creates a FileUploadIOException with the + * given cause. + * @param pCause The exceptions cause, if any, or null. + */ + public FileUploadIOException(FileUploadException pCause) { + // We're not doing super(pCause) cause of 1.3 compatibility. + cause = pCause; + } + + /** + * Returns the exceptions cause. + * @return The exceptions cause, if any, or null. + */ + public Throwable getCause() { + return cause; + } + } /** * Thrown to indicate that the request is not a multipart request. */ public static class InvalidContentTypeException - extends FileUploadException - { + extends FileUploadException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -9073026332015646668L; + /** * Constructs a InvalidContentTypeException with no * detail message. */ - public InvalidContentTypeException() - { - super(); + public InvalidContentTypeException() { + // Nothing to do. } /** @@ -576,25 +1109,106 @@ public abstract class FileUploadBase * * @param message The detail message. */ - public InvalidContentTypeException(String message) - { + public InvalidContentTypeException(String message) { super(message); } } + /** + * Thrown to indicate an IOException. + */ + public static class IOFileUploadException extends FileUploadException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = 1749796615868477269L; + /** The exceptions cause; we overwrite the parent + * classes field, which is available since Java + * 1.4 only. + */ + private final IOException cause; + + /** + * Creates a new instance with the given cause. + * @param pMsg The detail message. + * @param pException The exceptions cause. + */ + public IOFileUploadException(String pMsg, IOException pException) { + super(pMsg); + cause = pException; + } + + /** + * Returns the exceptions cause. + * @return The exceptions cause, if any, or null. + */ + public Throwable getCause() { + return cause; + } + } + + /** This exception is thrown, if a requests permitted size + * is exceeded. + */ + protected abstract static class SizeException extends FileUploadException { + /** + * The actual size of the request. + */ + private final long actual; + + /** + * The maximum permitted size of the request. + */ + private final long permitted; + + /** + * Creates a new instance. + * @param message The detail message. + * @param actual The actual number of bytes in the request. + * @param permitted The requests size limit, in bytes. + */ + protected SizeException(String message, long actual, long permitted) { + super(message); + this.actual = actual; + this.permitted = permitted; + } + + /** + * Retrieves the actual size of the request. + * + * @return The actual size of the request. + */ + public long getActualSize() { + return actual; + } + + /** + * Retrieves the permitted size of the request. + * + * @return The permitted size of the request. + */ + public long getPermittedSize() { + return permitted; + } + } /** - * Thrown to indicate that the request size is not specified. + * Thrown to indicate that the request size is not specified. In other + * words, it is thrown, if the content-length header is missing or + * contains the value -1. + * @deprecated As of commons-fileupload 1.2, the presence of a + * content-length header is no longer required. */ public static class UnknownSizeException - extends FileUploadException - { + extends FileUploadException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = 7062279004812015273L; + /** * Constructs a UnknownSizeException with no * detail message. */ - public UnknownSizeException() - { + public UnknownSizeException() { super(); } @@ -604,38 +1218,87 @@ public abstract class FileUploadBase * * @param message The detail message. */ - public UnknownSizeException(String message) - { + public UnknownSizeException(String message) { super(message); } } - /** * Thrown to indicate that the request size exceeds the configured maximum. */ public static class SizeLimitExceededException - extends FileUploadException - { + extends SizeException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -2474893167098052828L; + /** - * Constructs a SizeExceededException with no - * detail message. + * @deprecated Replaced by + * {@link #SizeLimitExceededException(String, long, long)} */ - public SizeLimitExceededException() - { - super(); + public SizeLimitExceededException() { + this(null, 0, 0); } /** - * Constructs an SizeExceededException with - * the specified detail message. + * @deprecated Replaced by + * {@link #SizeLimitExceededException(String, long, long)} + * @param message The exceptions detail message. + */ + public SizeLimitExceededException(String message) { + this(message, 0, 0); + } + + /** + * Constructs a SizeExceededException with + * the specified detail message, and actual and permitted sizes. * - * @param message The detail message. + * @param message The detail message. + * @param actual The actual request size. + * @param permitted The maximum permitted request size. */ - public SizeLimitExceededException(String message) - { - super(message); + public SizeLimitExceededException(String message, long actual, + long permitted) { + super(message, actual, permitted); + } + } + + /** + * Thrown to indicate that A files size exceeds the configured maximum. + */ + public static class FileSizeLimitExceededException + extends SizeException { + /** The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = 8150776562029630058L; + + /** + * Constructs a SizeExceededException with + * the specified detail message, and actual and permitted sizes. + * + * @param message The detail message. + * @param actual The actual request size. + * @param permitted The maximum permitted request size. + */ + public FileSizeLimitExceededException(String message, long actual, + long permitted) { + super(message, actual, permitted); } } + /** + * Returns the progress listener. + * @return The progress listener, if any, or null. + */ + public ProgressListener getProgressListener() { + return listener; + } + + /** + * Sets the progress listener. + * @param pListener The progress listener, if any. Defaults to null. + */ + public void setProgressListener(ProgressListener pListener) { + listener = pListener; + } } diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java index fd5def139..092ee340d 100644 --- a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java @@ -5,19 +5,20 @@ * 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.tomcat.util.http.fileupload; +import java.io.PrintStream; +import java.io.PrintWriter; + /** * Exception for errors encountered while processing the request. @@ -25,15 +26,23 @@ package org.apache.tomcat.util.http.fileupload; * @author John McNally * @version $Id$ */ -public class FileUploadException - extends Exception -{ +public class FileUploadException extends Exception { + /** + * Serial version UID, being used, if the exception + * is serialized. + */ + private static final long serialVersionUID = 8881893724388807504L; + /** + * The exceptions cause. We overwrite the cause of + * the super class, which isn't available in Java 1.3. + */ + private final Throwable cause; /** * Constructs a new FileUploadException without message. */ - public FileUploadException() - { + public FileUploadException() { + this(null, null); } /** @@ -42,8 +51,49 @@ public class FileUploadException * * @param msg the error message. */ - public FileUploadException(String msg) - { + public FileUploadException(final String msg) { + this(msg, null); + } + + /** + * Creates a new FileUploadException with the given + * detail message and cause. + * @param msg The exceptions detail message. + * @param cause The exceptions cause. + */ + public FileUploadException(String msg, Throwable cause) { super(msg); + this.cause = cause; + } + + /** + * Prints this throwable and its backtrace to the specified print stream. + * + * @param stream PrintStream to use for output + */ + public void printStackTrace(PrintStream stream) { + super.printStackTrace(stream); + if (cause != null) { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + /** + * Prints this throwable and its backtrace to the specified + * print writer. + * + * @param writer PrintWriter to use for output + */ + public void printStackTrace(PrintWriter writer) { + super.printStackTrace(writer); + if (cause != null) { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + public Throwable getCause() { + return cause; } } diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUtils.java b/java/org/apache/tomcat/util/http/fileupload/FileUtils.java new file mode 100644 index 000000000..e8af88597 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileUtils.java @@ -0,0 +1,229 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + + +/** + * General file manipulation utilities. + *

+ * Facilities are provided in the following areas: + *

    + *
  • writing to a file + *
  • reading from a file + *
  • make a directory including parent directories + *
  • copying files and directories + *
  • deleting files and directories + *
  • converting to and from a URL + *
  • listing files and directories by filter and extension + *
  • comparing file content + *
  • file last changed date + *
  • calculating a checksum + *
+ *

+ * Origin of code: Excalibur, Alexandria, Commons-Utils + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Jeremias Maerki + * @author Stephen Colebourne + * @author Ian Springer + * @author Chris Eldredge + * @author Jim Harrington + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id$ + */ +public class FileUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectory(directory); + if (!directory.delete()) { + String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + */ + public static void cleanDirectory(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDelete(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + + //----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • You get exceptions when a file or directory cannot be deleted. + * (java.io.File methods returns a boolean)
  • + *
+ * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent){ + throw new FileNotFoundException("File does not exist: " + file); + } + String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Schedules a file to be deleted when JVM exits. + * If file is directory delete it and all sub-directories. + * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the file is null + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDeleteOnExit(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectoryOnExit(file); + } else { + file.deleteOnExit(); + } + } + + /** + * Schedules a directory recursively for deletion on JVM exit. + * + * @param directory directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case deletion is unsuccessful + */ + private static void deleteDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectoryOnExit(directory); + directory.deleteOnExit(); + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case cleaning is unsuccessful + */ + private static void cleanDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDeleteOnExit(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/IOUtils.java b/java/org/apache/tomcat/util/http/fileupload/IOUtils.java new file mode 100644 index 000000000..103dfd47b --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/IOUtils.java @@ -0,0 +1,162 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** + * General IO stream manipulation utilities. + *

+ * This class provides static utility methods for input/output operations. + *

    + *
  • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
  • toXxx/read - these methods read data from a stream + *
  • write - these methods write data to a stream + *
  • copy - these methods copy all the data from one stream to another + *
  • contentEquals - these methods compare the content of two streams + *
+ *

+ * The byte-to-char methods and char-to-byte methods involve a conversion step. + * Two methods are provided in each case, one that uses the platform default + * encoding and the other which allows you to specify an encoding. You are + * encouraged to always specify an encoding because relying on the platform + * default can lead to unexpected results, for example when moving from + * development to production. + *

+ * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a BufferedInputStream + * or BufferedReader. The default buffer size of 4K has been shown + * to be efficient in tests. + *

+ * Wherever possible, the methods in this class do not flush or close + * the stream. This is to avoid making non-portable assumptions about the + * streams' origin and further use. Thus the caller is still responsible for + * closing streams after use. + *

+ * Origin of code: Excalibur. + * + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Stephen Colebourne + * @author Gareth Davis + * @author Ian Springer + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id$ + */ +public class IOUtils { + // NOTE: This class is focussed on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + /** + * The default buffer size to use. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Instances should NOT be constructed in standard programming. + */ + public IOUtils() { + super(); + } + + + /** + * Unconditionally close an InputStream. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copy bytes from an InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the byte count is too large + * @since Commons IO 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + + + + + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/LimitedInputStream.java b/java/org/apache/tomcat/util/http/fileupload/LimitedInputStream.java new file mode 100644 index 000000000..6d0a536ae --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/LimitedInputStream.java @@ -0,0 +1,155 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + + +/** + * An input stream, which limits its data size. This stream is + * used, if the content length is unknown. + */ +public abstract class LimitedInputStream + extends FilterInputStream implements Closeable { + /** + * The maximum size of an item, in bytes. + */ + private long sizeMax; + /** + * The current number of bytes. + */ + private long count; + /** + * Whether this stream is already closed. + */ + private boolean closed; + + /** + * Creates a new instance. + * @param pIn The input stream, which shall be limited. + * @param pSizeMax The limit; no more than this number of bytes + * shall be returned by the source stream. + */ + public LimitedInputStream(InputStream pIn, long pSizeMax) { + super(pIn); + sizeMax = pSizeMax; + } + + /** + * Called to indicate, that the input streams limit has + * been exceeded. + * @param pSizeMax The input streams limit, in bytes. + * @param pCount The actual number of bytes. + * @throws IOException The called method is expected + * to raise an IOException. + */ + protected abstract void raiseError(long pSizeMax, long pCount) + throws IOException; + + /** Called to check, whether the input streams + * limit is reached. + * @throws IOException The given limit is exceeded. + */ + private void checkLimit() throws IOException { + if (count > sizeMax) { + raiseError(sizeMax, count); + } + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

+ * This method + * simply performs in.read() and returns the result. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + public int read() throws IOException { + int res = super.read(); + if (res != -1) { + count++; + checkLimit(); + } + return res; + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. If len is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and 0 is returned. + *

+ * This method simply performs in.read(b, off, len) + * and returns the result. + * + * @param b the buffer into which the data is read. + * @param off The start offset in the destination array + * b. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + public int read(byte[] b, int off, int len) throws IOException { + int res = super.read(b, off, len); + if (res > 0) { + count += res; + checkLimit(); + } + return res; + } + + /** + * Returns, whether this stream is already closed. + * @return True, if the stream is closed, otherwise false. + * @throws IOException An I/O error occurred. + */ + public boolean isClosed() throws IOException { + return closed; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * This + * method simply performs in.close(). + * + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + public void close() throws IOException { + closed = true; + super.close(); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java index 446eef83d..5f03f4d5c 100644 --- a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java +++ b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java @@ -5,20 +5,17 @@ * 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.tomcat.util.http.fileupload; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -58,13 +55,13 @@ import java.io.UnsupportedEncodingException; * boundary token of the same length as the parent stream (see {@link * #setBoundary(byte[])}). * - *

Here is an exaple of usage of this class.
+ *

Here is an example of usage of this class.
* *

  *    try {
  *        MultipartStream multipartStream = new MultipartStream(input,
  *                                                              boundary);
- *        boolean nextPart = malitPartStream.skipPreamble();
+ *        boolean nextPart = multipartStream.skipPreamble();
  *        OutputStream output;
  *        while(nextPart) {
  *            header = chunks.readHeader();
@@ -87,13 +84,79 @@ import java.io.UnsupportedEncodingException;
  *
  * @version $Id$
  */
-public class MultipartStream
-{
+public class MultipartStream {
+    /**
+     * Internal class, which is used to invoke the
+     * {@link ProgressListener}.
+     */
+    static class ProgressNotifier {
+        /** The listener to invoke.
+         */
+        private final ProgressListener listener;
+        /** Number of expected bytes, if known, or -1.
+         */
+        private final long contentLength;
+        /** Number of bytes, which have been read so far.
+         */
+        private long bytesRead;
+        /** Number of items, which have been read so far.
+         */
+        private int items;
+        /** Creates a new instance with the given listener
+         * and content length.
+         * @param pListener The listener to invoke.
+         * @param pContentLength The expected content length.
+         */
+        ProgressNotifier(ProgressListener pListener, long pContentLength) {
+            listener = pListener;
+            contentLength = pContentLength;
+        }
+        /** Called to indicate that bytes have been read.
+         * @param pBytes Number of bytes, which have been read.
+         */
+        void noteBytesRead(int pBytes) {
+            /* Indicates, that the given number of bytes have been read from
+             * the input stream.
+             */
+            bytesRead += pBytes;
+            notifyListener();
+        }
+        /** Called to indicate, that a new file item has been detected.
+         */
+        void noteItem() {
+            ++items;
+        }
+        /** Called for notifying the listener.
+         */
+        private void notifyListener() {
+            if (listener != null) {
+                listener.update(bytesRead, contentLength, items);
+            }
+        }
+    }
 
     // ----------------------------------------------------- Manifest constants
 
 
     /**
+     * The Carriage Return ASCII character value.
+     */
+    public static final byte CR = 0x0D;
+
+
+    /**
+     * The Line Feed ASCII character value.
+     */
+    public static final byte LF = 0x0A;
+
+
+    /**
+     * The dash (-) ASCII character value.
+     */
+    public static final byte DASH = 0x2D;
+
+
+    /**
      * The maximum length of header-part that will be
      * processed (10 kilobytes = 10240 bytes.).
      */
@@ -110,21 +173,31 @@ public class MultipartStream
      * A byte sequence that marks the end of header-part
      * (CRLFCRLF).
      */
-    protected static final byte[] HEADER_SEPARATOR = {0x0D, 0x0A, 0x0D, 0x0A};
+    protected static final byte[] HEADER_SEPARATOR = {
+        CR, LF, CR, LF };
 
 
     /**
      * A byte sequence that that follows a delimiter that will be
      * followed by an encapsulation (CRLF).
      */
-    protected static final byte[] FIELD_SEPARATOR = { 0x0D, 0x0A };
+    protected static final byte[] FIELD_SEPARATOR = {
+        CR, LF};
 
 
     /**
      * A byte sequence that that follows a delimiter of the last
      * encapsulation in the stream (--).
      */
-    protected static final byte[] STREAM_TERMINATOR = { 0x2D, 0x2D };
+    protected static final byte[] STREAM_TERMINATOR = {
+        DASH, DASH};
+
+
+    /**
+     * A byte sequence that precedes a boundary (CRLF--).
+     */
+    protected static final byte[] BOUNDARY_PREFIX = {
+        CR, LF, DASH, DASH};
 
 
     // ----------------------------------------------------------- Data members
@@ -133,7 +206,7 @@ public class MultipartStream
     /**
      * The input stream from which data is read.
      */
-    private InputStream input;
+    private final InputStream input;
 
 
     /**
@@ -158,13 +231,13 @@ public class MultipartStream
     /**
      * The length of the buffer used for processing the request.
      */
-    private int bufSize;
+    private final int bufSize;
 
 
     /**
      * The buffer used for processing the request.
      */
-    private byte[] buffer;
+    private final byte[] buffer;
 
 
     /**
@@ -189,21 +262,47 @@ public class MultipartStream
     private String headerEncoding;
 
 
+    /**
+     * The progress notifier, if any, or null.
+     */
+    private final ProgressNotifier notifier;
+
     // ----------------------------------------------------------- Constructors
 
+    /**
+     * Creates a new instance.
+     * @deprecated Use {@link #MultipartStream(InputStream, byte[],
+     * org.apache.commons.fileupload.MultipartStream.ProgressNotifier)},
+     * or {@link #MultipartStream(InputStream, byte[], int,
+     * org.apache.commons.fileupload.MultipartStream.ProgressNotifier)}
+     */
+    public MultipartStream() {
+        this(null, null, null);
+    }
 
     /**
-     * Default constructor.
+     * 

Constructs a MultipartStream with a custom size buffer + * and no progress notifier. * - * @see #MultipartStream(InputStream, byte[], int) - * @see #MultipartStream(InputStream, byte[]) + *

Note that the buffer must be at least big enough to contain the + * boundary string, plus 4 characters for CR/LF and double dash, plus at + * least one byte of data. Too small a buffer size setting will degrade + * performance. * + * @param input The InputStream to serve as a data source. + * @param boundary The token used for dividing the stream into + * encapsulations. + * @param bufSize The size of the buffer to be used, in bytes. + * + * @see #MultipartStream(InputStream, byte[], + * MultipartStream.ProgressNotifier) + * @deprecated Use {@link #MultipartStream(InputStream, byte[], int, + * org.apache.commons.fileupload.MultipartStream.ProgressNotifier)}. */ - public MultipartStream() - { + public MultipartStream(InputStream input, byte[] boundary, int bufSize) { + this(input, boundary, bufSize, null); } - /** *

Constructs a MultipartStream with a custom size buffer. * @@ -216,30 +315,30 @@ public class MultipartStream * @param boundary The token used for dividing the stream into * encapsulations. * @param bufSize The size of the buffer to be used, in bytes. + * @param pNotifier The notifier, which is used for calling the + * progress listener, if any. * - * - * @see #MultipartStream() - * @see #MultipartStream(InputStream, byte[]) - * + * @see #MultipartStream(InputStream, byte[], + * MultipartStream.ProgressNotifier) */ - public MultipartStream(InputStream input, - byte[] boundary, - int bufSize) - { + MultipartStream(InputStream input, + byte[] boundary, + int bufSize, + ProgressNotifier pNotifier) { this.input = input; this.bufSize = bufSize; this.buffer = new byte[bufSize]; + this.notifier = pNotifier; // We prepend CR/LF to the boundary to chop trailng CR/LF from // body-data tokens. - this.boundary = new byte[boundary.length + 4]; - this.boundaryLength = boundary.length + 4; - this.keepRegion = boundary.length + 3; - this.boundary[0] = 0x0D; - this.boundary[1] = 0x0A; - this.boundary[2] = 0x2D; - this.boundary[3] = 0x2D; - System.arraycopy(boundary, 0, this.boundary, 4, boundary.length); + this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length]; + this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length; + this.keepRegion = this.boundary.length; + System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, + BOUNDARY_PREFIX.length); + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, + boundary.length); head = 0; tail = 0; @@ -252,21 +351,35 @@ public class MultipartStream * @param input The InputStream to serve as a data source. * @param boundary The token used for dividing the stream into * encapsulations. + * @param pNotifier An object for calling the progress listener, if any. + * * - * @exception IOException when an error occurs. + * @see #MultipartStream(InputStream, byte[], int, + * MultipartStream.ProgressNotifier) + */ + MultipartStream(InputStream input, + byte[] boundary, + ProgressNotifier pNotifier) { + this(input, boundary, DEFAULT_BUFSIZE, pNotifier); + } + + /** + *

Constructs a MultipartStream with a default size buffer. * - * @see #MultipartStream() - * @see #MultipartStream(InputStream, byte[], int) + * @param input The InputStream to serve as a data source. + * @param boundary The token used for dividing the stream into + * encapsulations. * + * @deprecated Use {@link #MultipartStream(InputStream, byte[], + * MultipartStream.ProgressNotifier)}. + * @see #MultipartStream(InputStream, byte[], int, + * MultipartStream.ProgressNotifier) */ public MultipartStream(InputStream input, - byte[] boundary) - throws IOException - { - this(input, boundary, DEFAULT_BUFSIZE); + byte[] boundary) { + this(input, boundary, DEFAULT_BUFSIZE, null); } - // --------------------------------------------------------- Public methods @@ -278,8 +391,7 @@ public class MultipartStream * * @return The encoding used to read part headers. */ - public String getHeaderEncoding() - { + public String getHeaderEncoding() { return headerEncoding; } @@ -291,8 +403,7 @@ public class MultipartStream * * @param encoding The encoding used to read part headers. */ - public void setHeaderEncoding(String encoding) - { + public void setHeaderEncoding(String encoding) { headerEncoding = encoding; } @@ -303,22 +414,21 @@ public class MultipartStream * * @return The next byte from the input stream. * - * @exception IOException if there is no more data available. + * @throws IOException if there is no more data available. */ - public byte readByte() - throws IOException - { + public byte readByte() throws IOException { // Buffer depleted ? - if (head == tail) - { + if (head == tail) { head = 0; // Refill. tail = input.read(buffer, head, bufSize); - if (tail == -1) - { + if (tail == -1) { // No more data available. throw new IOException("No more data is available"); } + if (notifier != null) { + notifier.noteBytesRead(tail); + } } return buffer[head++]; } @@ -331,36 +441,37 @@ public class MultipartStream * @return true if there are more encapsulations in * this stream; false otherwise. * - * @exception MalformedStreamException if the stream ends unexpecetedly or - * fails to follow required syntax. + * @throws MalformedStreamException if the stream ends unexpecetedly or + * fails to follow required syntax. */ public boolean readBoundary() - throws MalformedStreamException - { + throws MalformedStreamException { byte[] marker = new byte[2]; boolean nextChunk = false; head += boundaryLength; - try - { + try { marker[0] = readByte(); + if (marker[0] == LF) { + // Work around IE5 Mac bug with input type=image. + // Because the boundary delimiter, not including the trailing + // CRLF, must not appear within any file (RFC 2046, section + // 5.1.1), we know the missing CR is due to a buggy browser + // rather than a file containing something similar to a + // boundary. + return true; + } + marker[1] = readByte(); - if (arrayequals(marker, STREAM_TERMINATOR, 2)) - { + if (arrayequals(marker, STREAM_TERMINATOR, 2)) { nextChunk = false; - } - else if (arrayequals(marker, FIELD_SEPARATOR, 2)) - { + } else if (arrayequals(marker, FIELD_SEPARATOR, 2)) { nextChunk = true; - } - else - { + } else { throw new MalformedStreamException( - "Unexpected characters follow a boundary"); + "Unexpected characters follow a boundary"); } - } - catch (IOException e) - { + } catch (IOException e) { throw new MalformedStreamException("Stream ended unexpectedly"); } return nextChunk; @@ -382,19 +493,18 @@ public class MultipartStream * @param boundary The boundary to be used for parsing of the nested * stream. * - * @exception IllegalBoundaryException if the boundary - * has a different length than the one - * being currently parsed. + * @throws IllegalBoundaryException if the boundary + * has a different length than the one + * being currently parsed. */ public void setBoundary(byte[] boundary) - throws IllegalBoundaryException - { - if (boundary.length != boundaryLength - 4) - { + throws IllegalBoundaryException { + if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) { throw new IllegalBoundaryException( - "The length of a boundary token can not be changed"); + "The length of a boundary token can not be changed"); } - System.arraycopy(boundary, 0, this.boundary, 4, boundary.length); + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, + boundary.length); } @@ -411,58 +521,44 @@ public class MultipartStream * * @return The header-part of the current encapsulation. * - * @exception MalformedStreamException if the stream ends unexpecetedly. + * @throws MalformedStreamException if the stream ends unexpecetedly. */ public String readHeaders() - throws MalformedStreamException - { + throws MalformedStreamException { int i = 0; - byte b[] = new byte[1]; + byte b; // to support multi-byte characters ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int sizeMax = HEADER_PART_SIZE_MAX; int size = 0; - while (i < 4) - { - try - { - b[0] = readByte(); - } - catch (IOException e) - { + while (i < HEADER_SEPARATOR.length) { + try { + b = readByte(); + } catch (IOException e) { throw new MalformedStreamException("Stream ended unexpectedly"); } - size++; - if (b[0] == HEADER_SEPARATOR[i]) - { - i++; + if (++size > HEADER_PART_SIZE_MAX) { + throw new MalformedStreamException( + "Header section has more than " + HEADER_PART_SIZE_MAX + + " bytes (maybe it is not properly terminated)"); } - else - { + if (b == HEADER_SEPARATOR[i]) { + i++; + } else { i = 0; } - if (size <= sizeMax) - { - baos.write(b[0]); - } + baos.write(b); } String headers = null; - if (headerEncoding != null) - { - try - { + if (headerEncoding != null) { + try { headers = baos.toString(headerEncoding); - } - catch (UnsupportedEncodingException e) - { + } catch (UnsupportedEncodingException e) { // Fall back to platform default if specified encoding is not // supported. headers = baos.toString(); } - } - else - { + } else { headers = baos.toString(); } @@ -477,81 +573,31 @@ public class MultipartStream * *

Arbitrary large amounts of data can be processed by this * method using a constant size buffer. (see {@link - * #MultipartStream(InputStream,byte[],int) constructor}). + * #MultipartStream(InputStream,byte[],int, + * MultipartStream.ProgressNotifier) constructor}). * - * @param output The Stream to write data into. + * @param output The Stream to write data into. May + * be null, in which case this method is equivalent + * to {@link #discardBodyData()}. * * @return the amount of data written. * - * @exception MalformedStreamException if the stream ends unexpectedly. - * @exception IOException if an i/o error occurs. + * @throws MalformedStreamException if the stream ends unexpectedly. + * @throws IOException if an i/o error occurs. */ public int readBodyData(OutputStream output) - throws MalformedStreamException, - IOException - { - boolean done = false; - int pad; - int pos; - int bytesRead; - int total = 0; - while (!done) - { - // Is boundary token present somewere in the buffer? - pos = findSeparator(); - if (pos != -1) - { - // Write the rest of the data before the boundary. - output.write(buffer, head, pos - head); - total += pos - head; - head = pos; - done = true; - } - else - { - // Determine how much data should be kept in the - // buffer. - if (tail - head > keepRegion) - { - pad = keepRegion; - } - else - { - pad = tail - head; - } - // Write out the data belonging to the body-data. - output.write(buffer, head, tail - head - pad); - - // Move the data to the beging of the buffer. - total += tail - head - pad; - System.arraycopy(buffer, tail - pad, buffer, 0, pad); - - // Refill buffer with new data. - head = 0; - bytesRead = input.read(buffer, pad, bufSize - pad); - - // [pprrrrrrr] - if (bytesRead != -1) - { - tail = pad + bytesRead; - } - else - { - // The last pad amount is left in the buffer. - // Boundary can't be in there so write out the - // data you have and signal an error condition. - output.write(buffer, 0, pad); - output.flush(); - total += pad; - throw new MalformedStreamException( - "Stream ended unexpectedly"); - } - } - } - output.flush(); - return total; + throws MalformedStreamException, IOException { + final InputStream istream = newInputStream(); + return (int) Streams.copy(istream, output, false); } + /** + * Creates a new {@link ItemInputStream}. + * @return A new instance of {@link ItemInputStream}. + */ + ItemInputStream newInputStream() { + return new ItemInputStream(); + } /** *

Reads body-data from the current @@ -562,67 +608,13 @@ public class MultipartStream * * @return The amount of data discarded. * - * @exception MalformedStreamException if the stream ends unexpectedly. - * @exception IOException if an i/o error occurs. + * @throws MalformedStreamException if the stream ends unexpectedly. + * @throws IOException if an i/o error occurs. */ public int discardBodyData() - throws MalformedStreamException, - IOException - { - boolean done = false; - int pad; - int pos; - int bytesRead; - int total = 0; - while (!done) - { - // Is boundary token present somewere in the buffer? - pos = findSeparator(); - if (pos != -1) - { - // Write the rest of the data before the boundary. - total += pos - head; - head = pos; - done = true; - } - else - { - // Determine how much data should be kept in the - // buffer. - if (tail - head > keepRegion) - { - pad = keepRegion; - } - else - { - pad = tail - head; - } - total += tail - head - pad; - - // Move the data to the beging of the buffer. - System.arraycopy(buffer, tail - pad, buffer, 0, pad); - - // Refill buffer with new data. - head = 0; - bytesRead = input.read(buffer, pad, bufSize - pad); - - // [pprrrrrrr] - if (bytesRead != -1) - { - tail = pad + bytesRead; - } - else - { - // The last pad amount is left in the buffer. - // Boundary can't be in there so signal an error - // condition. - total += pad; - throw new MalformedStreamException( - "Stream ended unexpectedly"); - } - } - } - return total; + throws MalformedStreamException, + IOException { + return readBodyData(null); } @@ -632,34 +624,28 @@ public class MultipartStream * @return true if an encapsulation was found in * the stream. * - * @exception IOException if an i/o error occurs. + * @throws IOException if an i/o error occurs. */ public boolean skipPreamble() - throws IOException - { + throws IOException { // First delimiter may be not preceeded with a CRLF. System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2); boundaryLength = boundary.length - 2; - try - { + try { // Discard all data up to the delimiter. discardBodyData(); // Read boundary - if succeded, the stream contains an // encapsulation. return readBoundary(); - } - catch (MalformedStreamException e) - { + } catch (MalformedStreamException e) { return false; - } - finally - { + } finally { // Restore delimiter. System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2); boundaryLength = boundary.length; - boundary[0] = 0x0D; - boundary[1] = 0x0A; + boundary[0] = CR; + boundary[1] = LF; } } @@ -676,13 +662,10 @@ public class MultipartStream * a and b are equal. */ public static boolean arrayequals(byte[] a, - byte[] b, - int count) - { - for (int i = 0; i < count; i++) - { - if (a[i] != b[i]) - { + byte[] b, + int count) { + for (int i = 0; i < count; i++) { + if (a[i] != b[i]) { return false; } } @@ -701,12 +684,9 @@ public class MultipartStream * buffer, or -1 if not found. */ protected int findByte(byte value, - int pos) - { - for (int i = pos; i < tail; i++) - { - if (buffer[i] == value) - { + int pos) { + for (int i = pos; i < tail; i++) { + if (buffer[i] == value) { return i; } } @@ -723,64 +703,40 @@ public class MultipartStream * beginning of the buffer, or -1 if * not found. */ - protected int findSeparator() - { + protected int findSeparator() { int first; int match = 0; int maxpos = tail - boundaryLength; for (first = head; - (first <= maxpos) && (match != boundaryLength); - first++) - { + (first <= maxpos) && (match != boundaryLength); + first++) { first = findByte(boundary[0], first); - if (first == -1 || (first > maxpos)) - { + if (first == -1 || (first > maxpos)) { return -1; } - for (match = 1; match < boundaryLength; match++) - { - if (buffer[first + match] != boundary[match]) - { + for (match = 1; match < boundaryLength; match++) { + if (buffer[first + match] != boundary[match]) { break; } } } - if (match == boundaryLength) - { + if (match == boundaryLength) { return first - 1; } return -1; } /** - * Returns a string representation of this object. - * - * @return The string representation of this object. - */ - @Override - public String toString() - { - StringBuilder sbTemp = new StringBuilder(); - sbTemp.append("boundary='"); - sbTemp.append(String.valueOf(boundary)); - sbTemp.append("'\nbufSize="); - sbTemp.append(bufSize); - return sbTemp.toString(); - } - - /** * Thrown to indicate that the input stream fails to follow the * required syntax. */ - public class MalformedStreamException - extends IOException - { + public static class MalformedStreamException + extends IOException { /** * Constructs a MalformedStreamException with no * detail message. */ - public MalformedStreamException() - { + public MalformedStreamException() { super(); } @@ -790,8 +746,7 @@ public class MultipartStream * * @param message The detail message. */ - public MalformedStreamException(String message) - { + public MalformedStreamException(String message) { super(message); } } @@ -800,15 +755,13 @@ public class MultipartStream /** * Thrown upon attempt of setting an invalid boundary token. */ - public class IllegalBoundaryException - extends IOException - { + public static class IllegalBoundaryException + extends IOException { /** * Constructs an IllegalBoundaryException with no * detail message. */ - public IllegalBoundaryException() - { + public IllegalBoundaryException() { super(); } @@ -818,12 +771,237 @@ public class MultipartStream * * @param message The detail message. */ - public IllegalBoundaryException(String message) - { + public IllegalBoundaryException(String message) { super(message); } } + /** + * An {@link InputStream} for reading an items contents. + */ + public class ItemInputStream extends InputStream implements Closeable { + /** The number of bytes, which have been read so far. + */ + private long total; + /** The number of bytes, which must be hold, because + * they might be a part of the boundary. + */ + private int pad; + /** The current offset in the buffer. + */ + private int pos; + /** Whether the stream is already closed. + */ + private boolean closed; + + /** + * Creates a new instance. + */ + ItemInputStream() { + findSeparator(); + } + + /** + * Called for finding the separator. + */ + private void findSeparator() { + pos = MultipartStream.this.findSeparator(); + if (pos == -1) { + if (tail - head > keepRegion) { + pad = keepRegion; + } else { + pad = tail - head; + } + } + } + + /** + * Returns the number of bytes, which have been read + * by the stream. + * @return Number of bytes, which have been read so far. + */ + public long getBytesRead() { + return total; + } + + /** + * Returns the number of bytes, which are currently + * available, without blocking. + * @throws IOException An I/O error occurs. + * @return Number of bytes in the buffer. + */ + public int available() throws IOException { + if (pos == -1) { + return tail - head - pad; + } + return pos - head; + } + + /** Offset when converting negative bytes to integers. + */ + private static final int BYTE_POSITIVE_OFFSET = 256; + + /** + * Returns the next byte in the stream. + * @return The next byte in the stream, as a non-negative + * integer, or -1 for EOF. + * @throws IOException An I/O error occurred. + */ + public int read() throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + if (available() == 0) { + if (makeAvailable() == 0) { + return -1; + } + } + ++total; + int b = buffer[head++]; + if (b >= 0) { + return b; + } + return b + BYTE_POSITIVE_OFFSET; + } + + /** + * Reads bytes into the given buffer. + * @param b The destination buffer, where to write to. + * @param off Offset of the first byte in the buffer. + * @param len Maximum number of bytes to read. + * @return Number of bytes, which have been actually read, + * or -1 for EOF. + * @throws IOException An I/O error occurred. + */ + public int read(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + if (len == 0) { + return 0; + } + int res = available(); + if (res == 0) { + res = makeAvailable(); + if (res == 0) { + return -1; + } + } + res = Math.min(res, len); + System.arraycopy(buffer, head, b, off, res); + head += res; + total += res; + return res; + } + + /** + * Closes the input stream. + * @throws IOException An I/O error occurred. + */ + public void close() throws IOException { + close(false); + } + + /** + * Closes the input stream. + * @param pCloseUnderlying Whether to close the underlying stream + * (hard close) + * @throws IOException An I/O error occurred. + */ + public void close(boolean pCloseUnderlying) throws IOException { + if (closed) { + return; + } + if (pCloseUnderlying) { + closed = true; + input.close(); + } else { + for (;;) { + int av = available(); + if (av == 0) { + av = makeAvailable(); + if (av == 0) { + break; + } + } + skip(av); + } + } + closed = true; + } + + /** + * Skips the given number of bytes. + * @param bytes Number of bytes to skip. + * @return The number of bytes, which have actually been + * skipped. + * @throws IOException An I/O error occurred. + */ + public long skip(long bytes) throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + int av = available(); + if (av == 0) { + av = makeAvailable(); + if (av == 0) { + return 0; + } + } + long res = Math.min(av, bytes); + head += res; + return res; + } + + /** + * Attempts to read more data. + * @return Number of available bytes + * @throws IOException An I/O error occurred. + */ + private int makeAvailable() throws IOException { + if (pos != -1) { + return 0; + } + + // Move the data to the beginning of the buffer. + total += tail - head - pad; + System.arraycopy(buffer, tail - pad, buffer, 0, pad); + + // Refill buffer with new data. + head = 0; + tail = pad; + + for (;;) { + int bytesRead = input.read(buffer, tail, bufSize - tail); + if (bytesRead == -1) { + // The last pad amount is left in the buffer. + // Boundary can't be in there so signal an error + // condition. + final String msg = "Stream ended unexpectedly"; + throw new MalformedStreamException(msg); + } + if (notifier != null) { + notifier.noteBytesRead(bytesRead); + } + tail += bytesRead; + + findSeparator(); + int av = available(); + + if (av > 0 || pos != -1) { + return av; + } + } + } + + /** + * Returns, whether the stream is closed. + * @return True, if the stream is closed, otherwise false. + */ + public boolean isClosed() { + return closed; + } + } // ------------------------------------------------------ Debugging methods @@ -861,7 +1039,7 @@ public class MultipartStream // Main routine, for testing purposes only. // // @param args A String[] with the command line arguments. - // @exception Exception, a generic exception. + // @throws Exception, a generic exception. public static void main( String[] args ) throws Exception { @@ -889,5 +1067,5 @@ public class MultipartStream } } - */ + */ } diff --git a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java new file mode 100644 index 000000000..dce150777 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java @@ -0,0 +1,329 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.util.HashMap; +import java.util.Map; + +/** + * A simple parser intended to parse sequences of name/value pairs. + * Parameter values are exptected to be enclosed in quotes if they + * contain unsafe characters, such as '=' characters or separators. + * Parameter values are optional and can be omitted. + * + *

+ * param1 = value; param2 = "anything goes; really"; param3 + *

+ * + * @author Oleg Kalnichevski + */ + +public class ParameterParser { + /** + * String to be parsed. + */ + private char[] chars = null; + + /** + * Current position in the string. + */ + private int pos = 0; + + /** + * Maximum position in the string. + */ + private int len = 0; + + /** + * Start of a token. + */ + private int i1 = 0; + + /** + * End of a token. + */ + private int i2 = 0; + + /** + * Whether names stored in the map should be converted to lower case. + */ + private boolean lowerCaseNames = false; + + /** + * Default ParameterParser constructor. + */ + public ParameterParser() { + super(); + } + + /** + * Are there any characters left to parse? + * + * @return true if there are unparsed characters, + * false otherwise. + */ + private boolean hasChar() { + return this.pos < this.len; + } + + /** + * A helper method to process the parsed token. This method removes + * leading and trailing blanks as well as enclosing quotation marks, + * when necessary. + * + * @param quoted true if quotation marks are expected, + * false otherwise. + * @return the token + */ + private String getToken(boolean quoted) { + // Trim leading white spaces + while ((i1 < i2) && (Character.isWhitespace(chars[i1]))) { + i1++; + } + // Trim trailing white spaces + while ((i2 > i1) && (Character.isWhitespace(chars[i2 - 1]))) { + i2--; + } + // Strip away quotation marks if necessary + if (quoted) { + if (((i2 - i1) >= 2) + && (chars[i1] == '"') + && (chars[i2 - 1] == '"')) { + i1++; + i2--; + } + } + String result = null; + if (i2 > i1) { + result = new String(chars, i1, i2 - i1); + } + return result; + } + + /** + * Tests if the given character is present in the array of characters. + * + * @param ch the character to test for presense in the array of characters + * @param charray the array of characters to test against + * + * @return true if the character is present in the array of + * characters, false otherwise. + */ + private boolean isOneOf(char ch, final char[] charray) { + boolean result = false; + for (int i = 0; i < charray.length; i++) { + if (ch == charray[i]) { + result = true; + break; + } + } + return result; + } + + /** + * Parses out a token until any of the given terminators + * is encountered. + * + * @param terminators the array of terminating characters. Any of these + * characters when encountered signify the end of the token + * + * @return the token + */ + private String parseToken(final char[] terminators) { + char ch; + i1 = pos; + i2 = pos; + while (hasChar()) { + ch = chars[pos]; + if (isOneOf(ch, terminators)) { + break; + } + i2++; + pos++; + } + return getToken(false); + } + + /** + * Parses out a token until any of the given terminators + * is encountered outside the quotation marks. + * + * @param terminators the array of terminating characters. Any of these + * characters when encountered outside the quotation marks signify the end + * of the token + * + * @return the token + */ + private String parseQuotedToken(final char[] terminators) { + char ch; + i1 = pos; + i2 = pos; + boolean quoted = false; + boolean charEscaped = false; + while (hasChar()) { + ch = chars[pos]; + if (!quoted && isOneOf(ch, terminators)) { + break; + } + if (!charEscaped && ch == '"') { + quoted = !quoted; + } + charEscaped = (!charEscaped && ch == '\\'); + i2++; + pos++; + + } + return getToken(true); + } + + /** + * Returns true if parameter names are to be converted to lower + * case when name/value pairs are parsed. + * + * @return true if parameter names are to be + * converted to lower case when name/value pairs are parsed. + * Otherwise returns false + */ + public boolean isLowerCaseNames() { + return this.lowerCaseNames; + } + + /** + * Sets the flag if parameter names are to be converted to lower case when + * name/value pairs are parsed. + * + * @param b true if parameter names are to be + * converted to lower case when name/value pairs are parsed. + * false otherwise. + */ + public void setLowerCaseNames(boolean b) { + this.lowerCaseNames = b; + } + + /** + * Extracts a map of name/value pairs from the given string. Names are + * expected to be unique. Multiple separators may be specified and + * the earliest found in the input string is used. + * + * @param str the string that contains a sequence of name/value pairs + * @param separators the name/value pairs separators + * + * @return a map of name/value pairs + */ + public Map parse(final String str, char[] separators) { + if (separators == null || separators.length == 0) { + return new HashMap(); + } + char separator = separators[0]; + if (str != null) { + int idx = str.length(); + for (int i = 0; i < separators.length; i++) { + int tmp = str.indexOf(separators[i]); + if (tmp != -1) { + if (tmp < idx) { + idx = tmp; + separator = separators[i]; + } + } + } + } + return parse(str, separator); + } + + /** + * Extracts a map of name/value pairs from the given string. Names are + * expected to be unique. + * + * @param str the string that contains a sequence of name/value pairs + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse(final String str, char separator) { + if (str == null) { + return new HashMap(); + } + return parse(str.toCharArray(), separator); + } + + /** + * Extracts a map of name/value pairs from the given array of + * characters. Names are expected to be unique. + * + * @param chars the array of characters that contains a sequence of + * name/value pairs + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse(final char[] chars, char separator) { + if (chars == null) { + return new HashMap(); + } + return parse(chars, 0, chars.length, separator); + } + + /** + * Extracts a map of name/value pairs from the given array of + * characters. Names are expected to be unique. + * + * @param chars the array of characters that contains a sequence of + * name/value pairs + * @param offset - the initial offset. + * @param length - the length. + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse( + final char[] chars, + int offset, + int length, + char separator) { + + if (chars == null) { + return new HashMap(); + } + HashMap params = new HashMap(); + this.chars = chars; + this.pos = offset; + this.len = length; + + String paramName = null; + String paramValue = null; + while (hasChar()) { + paramName = parseToken(new char[] { + '=', separator }); + paramValue = null; + if (hasChar() && (chars[pos] == '=')) { + pos++; // skip '=' + paramValue = parseQuotedToken(new char[] { + separator }); + } + if (hasChar() && (chars[pos] == separator)) { + pos++; // skip separator + } + if ((paramName != null) && (paramName.length() > 0)) { + if (this.lowerCaseNames) { + paramName = paramName.toLowerCase(); + } + params.put(paramName, paramValue); + } + } + return params; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java new file mode 100644 index 000000000..61744caab --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.util.http.fileupload; + + +/** + * The {@link ProgressListener} may be used to display a progress bar + * or do stuff like that. + */ +public interface ProgressListener { + /** Updates the listeners status information. + * @param pBytesRead The total number of bytes, which have been read + * so far. + * @param pContentLength The total number of bytes, which are being + * read. May be -1, if this number is unknown. + * @param pItems The number of the field, which is currently being + * read. (0 = no item so far, 1 = first item is being read, ...) + */ + void update(long pBytesRead, long pContentLength, int pItems); +} diff --git a/java/org/apache/tomcat/util/http/fileupload/RequestContext.java b/java/org/apache/tomcat/util/http/fileupload/RequestContext.java new file mode 100644 index 000000000..370d86a95 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/RequestContext.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.InputStream; +import java.io.IOException; + +/** + *

Abstracts access to the request information needed for file uploads. This + * interfsace should be implemented for each type of request that may be + * handled by FileUpload, such as servlets and portlets.

+ * + * @author Martin Cooper + * + * @since FileUpload 1.1 + * + * @version $Id$ + */ +public interface RequestContext { + + /** + * Retrieve the character encoding for the request. + * + * @return The character encoding for the request. + */ + String getCharacterEncoding(); + + /** + * Retrieve the content type of the request. + * + * @return The content type of the request. + */ + String getContentType(); + + /** + * Retrieve the content length of the request. + * + * @return The content length of the request. + */ + int getContentLength(); + + /** + * Retrieve the input stream for the request. + * + * @return The input stream for the request. + * + * @throws IOException if a problem occurs. + */ + InputStream getInputStream() throws IOException; +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ServletFileUpload.java b/java/org/apache/tomcat/util/http/fileupload/ServletFileUpload.java new file mode 100644 index 000000000..589a22935 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ServletFileUpload.java @@ -0,0 +1,146 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + + +/** + *

High level API for processing file uploads.

+ * + *

This class handles multiple files per single HTML widget, sent using + * multipart/mixed encoding type, as specified by + * RFC 1867. Use {@link + * #parseRequest(HttpServletRequest)} to acquire a list of {@link + * org.apache.commons.fileupload.FileItem}s associated with a given HTML + * widget.

+ * + *

How the data for individual parts is stored is determined by the factory + * used to create them; a given part may be in memory, on disk, or somewhere + * else.

+ * + * @author Rafal Krzewski + * @author Daniel Rall + * @author Jason van Zyl + * @author John McNally + * @author Martin Cooper + * @author Sean C. Sullivan + * + * @version $Id$ + */ +public class ServletFileUpload extends FileUpload { + + // ---------------------------------------------------------- Class methods + + + /** + * Utility method that determines whether the request contains multipart + * content. + * + * @param request The servlet request to be evaluated. Must be non-null. + * + * @return true if the request is multipart; + * false otherwise. + */ + public static final boolean isMultipartContent( + HttpServletRequest request) { + if (!"post".equals(request.getMethod().toLowerCase())) { + return false; + } + String contentType = request.getContentType(); + if (contentType == null) { + return false; + } + if (contentType.toLowerCase().startsWith(MULTIPART)) { + return true; + } + return false; + } + + + // ----------------------------------------------------------- Constructors + + + /** + * Constructs an uninitialised instance of this class. A factory must be + * configured, using setFileItemFactory(), before attempting + * to parse requests. + * + * @see FileUpload#FileUpload(FileItemFactory) + */ + public ServletFileUpload() { + super(); + } + + + /** + * Constructs an instance of this class which uses the supplied factory to + * create FileItem instances. + * + * @see FileUpload#FileUpload() + * @param fileItemFactory The factory to use for creating file items. + */ + public ServletFileUpload(FileItemFactory fileItemFactory) { + super(fileItemFactory); + } + + + // --------------------------------------------------------- Public methods + + + /** + * Processes an RFC 1867 + * compliant multipart/form-data stream. + * + * @param request The servlet request to be parsed. + * + * @return A list of FileItem instances parsed from the + * request, in the order that they were transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + */ + public List /* FileItem */ parseRequest(HttpServletRequest request) + throws FileUploadException { + return parseRequest(new ServletRequestContext(request)); + } + + + /** + * Processes an RFC 1867 + * compliant multipart/form-data stream. + * + * @param request The servlet request to be parsed. + * + * @return An iterator to instances of FileItemStream + * parsed from the request, in the order that they were + * transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + * @throws IOException An I/O error occurred. This may be a network + * error while communicating with the client or a problem while + * storing the uploaded content. + */ + public FileItemIterator getItemIterator(HttpServletRequest request) + throws FileUploadException, IOException { + return super.getItemIterator(new ServletRequestContext(request)); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ServletRequestContext.java b/java/org/apache/tomcat/util/http/fileupload/ServletRequestContext.java new file mode 100644 index 000000000..7fdaedcc2 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ServletRequestContext.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.InputStream; +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; + + +/** + *

Provides access to the request information needed for a request made to + * an HTTP servlet.

+ * + * @author Martin Cooper + * + * @since FileUpload 1.1 + * + * @version $Id$ + */ +public class ServletRequestContext implements RequestContext { + + // ----------------------------------------------------- Instance Variables + + /** + * The request for which the context is being provided. + */ + private HttpServletRequest request; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a context for this request. + * + * @param request The request to which this context applies. + */ + public ServletRequestContext(HttpServletRequest request) { + this.request = request; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Retrieve the character encoding for the request. + * + * @return The character encoding for the request. + */ + public String getCharacterEncoding() { + return request.getCharacterEncoding(); + } + + /** + * Retrieve the content type of the request. + * + * @return The content type of the request. + */ + public String getContentType() { + return request.getContentType(); + } + + /** + * Retrieve the content length of the request. + * + * @return The content length of the request. + */ + public int getContentLength() { + return request.getContentLength(); + } + + /** + * Retrieve the input stream for the request. + * + * @return The input stream for the request. + * + * @throws IOException if a problem occurs. + */ + public InputStream getInputStream() throws IOException { + return request.getInputStream(); + } + + /** + * Returns a string representation of this object. + * + * @return a string representation of this object. + */ + public String toString() { + return "ContentLength=" + + this.getContentLength() + + ", ContentType=" + + this.getContentType(); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/Streams.java b/java/org/apache/tomcat/util/http/fileupload/Streams.java new file mode 100644 index 000000000..d89f9373e --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/Streams.java @@ -0,0 +1,166 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** Utility class for working with streams. + */ +public final class Streams { + /** + * Private constructor, to prevent instantiation. + * This class has only static methods. + */ + private Streams() { + // Does nothing + } + + /** + * Default buffer size for use in + * {@link #copy(InputStream, OutputStream, boolean)}. + */ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** + * Copies the contents of the given {@link InputStream} + * to the given {@link OutputStream}. Shortcut for + *
+     *   copy(pInputStream, pOutputStream, new byte[8192]);
+     * 
+ * @param pInputStream The input stream, which is being read. + * It is guaranteed, that {@link InputStream#close()} is called + * on the stream. + * @param pOutputStream The output stream, to which data should + * be written. May be null, in which case the input streams + * contents are simply discarded. + * @param pClose True guarantees, that {@link OutputStream#close()} + * is called on the stream. False indicates, that only + * {@link OutputStream#flush()} should be called finally. + * + * @return Number of bytes, which have been copied. + * @throws IOException An I/O error occurred. + */ + public static long copy(InputStream pInputStream, + OutputStream pOutputStream, boolean pClose) + throws IOException { + return copy(pInputStream, pOutputStream, pClose, + new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies the contents of the given {@link InputStream} + * to the given {@link OutputStream}. + * @param pIn The input stream, which is being read. + * It is guaranteed, that {@link InputStream#close()} is called + * on the stream. + * @param pOut The output stream, to which data should + * be written. May be null, in which case the input streams + * contents are simply discarded. + * @param pClose True guarantees, that {@link OutputStream#close()} + * is called on the stream. False indicates, that only + * {@link OutputStream#flush()} should be called finally. + * @param pBuffer Temporary buffer, which is to be used for + * copying data. + * @return Number of bytes, which have been copied. + * @throws IOException An I/O error occurred. + */ + public static long copy(InputStream pIn, + OutputStream pOut, boolean pClose, + byte[] pBuffer) + throws IOException { + OutputStream out = pOut; + InputStream in = pIn; + try { + long total = 0; + for (;;) { + int res = in.read(pBuffer); + if (res == -1) { + break; + } + if (res > 0) { + total += res; + if (out != null) { + out.write(pBuffer, 0, res); + } + } + } + if (out != null) { + if (pClose) { + out.close(); + } else { + out.flush(); + } + out = null; + } + in.close(); + in = null; + return total; + } finally { + if (in != null) { + try { + in.close(); + } catch (Throwable t) { + /* Ignore me */ + } + } + if (pClose && out != null) { + try { + out.close(); + } catch (Throwable t) { + /* Ignore me */ + } + } + } + } + + /** + * This convenience method allows to read a + * {@link org.apache.commons.fileupload.FileItemStream}'s + * content into a string. The platform's default character encoding + * is used for converting bytes into characters. + * @param pStream The input stream to read. + * @see #asString(InputStream, String) + * @return The streams contents, as a string. + * @throws IOException An I/O error occurred. + */ + public static String asString(InputStream pStream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + copy(pStream, baos, true); + return baos.toString(); + } + + /** + * This convenience method allows to read a + * {@link org.apache.commons.fileupload.FileItemStream}'s + * content into a string, using the given character encoding. + * @param pStream The input stream to read. + * @param pEncoding The character encoding, typically "UTF-8". + * @see #asString(InputStream) + * @return The streams contents, as a string. + * @throws IOException An I/O error occurred. + */ + public static String asString(InputStream pStream, String pEncoding) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + copy(pStream, baos, true); + return baos.toString(pEncoding); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java index 54271fa91..142cb4012 100644 --- a/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java +++ b/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java @@ -14,8 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.tomcat.util.http.fileupload; import java.io.IOException; @@ -90,7 +88,6 @@ public abstract class ThresholdingOutputStream * * @exception IOException if an error occurs. */ - @Override public void write(int b) throws IOException { checkThreshold(1); @@ -107,7 +104,6 @@ public abstract class ThresholdingOutputStream * * @exception IOException if an error occurs. */ - @Override public void write(byte b[]) throws IOException { checkThreshold(b.length); @@ -126,7 +122,6 @@ public abstract class ThresholdingOutputStream * * @exception IOException if an error occurs. */ - @Override public void write(byte b[], int off, int len) throws IOException { checkThreshold(len); @@ -141,7 +136,6 @@ public abstract class ThresholdingOutputStream * * @exception IOException if an error occurs. */ - @Override public void flush() throws IOException { getStream().flush(); @@ -154,7 +148,6 @@ public abstract class ThresholdingOutputStream * * @exception IOException if an error occurs. */ - @Override public void close() throws IOException { try @@ -224,11 +217,20 @@ public abstract class ThresholdingOutputStream { if (!thresholdExceeded && (written + count > threshold)) { - thresholdReached(); thresholdExceeded = true; + thresholdReached(); } } + /** + * Resets the byteCount to zero. You can call this from + * {@link #thresholdReached()} if you want the event to be triggered again. + */ + protected void resetByteCount() + { + this.thresholdExceeded = false; + this.written = 0; + } // ------------------------------------------------------- Abstract methods diff --git a/java/org/apache/tomcat/util/http/fileupload/package.html b/java/org/apache/tomcat/util/http/fileupload/package.html index 0aa24add9..88c2540d2 100644 --- a/java/org/apache/tomcat/util/http/fileupload/package.html +++ b/java/org/apache/tomcat/util/http/fileupload/package.html @@ -1,57 +1,65 @@ - - Overview of the org.apache.commons.fileupload component - - -

- Component for handling html file uploads as given by rfc 1867 - RFC 1867. -

-

- Normal usage of the package involves - {@link org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} - parsing the HttpServletRequest and returning a list of - {@link org.apache.commons.fileupload.FileItem FileItem}'s. - These FileItem's provide easy access to the data - given in the upload. There is also a low level api for - manipulating the upload data encapsulated in the - {@link org.apache.commons.fileupload.MultipartStream MultipartStream} - class. -

- -

- Normal usage example: -

+ + Overview of the org.apache.commons.fileupload component + + +

NOTE: This code has been copied from commons-fileupload 1.2.1 and + commons-io 1.4 and package renamed to avoid clashes with any web apps that + may wish to use these libraries. +

+

+ A component for handling HTML file uploads as specified by + RFC 1867. + This component provides support for uploads within both servlets (JSR 53) + and portlets (JSR 168). +

+

+ While this package provides the generic functionality for file uploads, + these classes are not typically used directly. Instead, normal usage + involves one of the provided extensions of + {@link org.apache.commons.fileupload.FileUpload FileUpload} such as + {@link org.apache.commons.fileupload.servlet.ServletFileUpload ServletFileUpload} + or + {@link org.apache.commons.fileupload.portlet.PortletFileUpload PortletFileUpload}, + together with a factory for + {@link org.apache.commons.fileupload.FileItem FileItem} instances, + such as + {@link org.apache.commons.fileupload.disk.DiskFileItemFactory DiskFileItemFactory}. +

+

+ The following is a brief example of typical usage in a servlet, storing + the uploaded files on disk. +

-
-    public void doPost(HttpServletRequest req, HttpServletResponse res)
-    {
-        DiskFileUpload fu = new DiskFileUpload();
-        // maximum size before a FileUploadException will be thrown
-        fu.setSizeMax(1000000);
+    public void doPost(HttpServletRequest req, HttpServletResponse res) {
+        DiskFileItemFactory factory = new DiskFileItemFactory();
         // maximum size that will be stored in memory
-        fu.setSizeThreshold(4096);
+        factory.setSizeThreshold(4096);
         // the location for saving data that is larger than getSizeThreshold()
-        fu.setRepositoryPath("/tmp");
+        factory.setRepository(new File("/tmp"));
+
+        ServletFileUpload upload = new ServletFileUpload(factory);
+        // maximum size before a FileUploadException will be thrown
+        upload.setSizeMax(1000000);
 
-        List fileItems = fu.parseRequest(req);
+        List fileItems = upload.parseRequest(req);
         // assume we know there are two files. The first file is a small
         // text file, the second is unknown and is written to a file on
         // the server
@@ -63,20 +71,24 @@
         // save comment and filename to database
         ...
         // write the file
-        fi.write("/www/uploads/" + fileName);
+        fi.write(new File("/www/uploads/", fileName));
     }
 
-

- In the example above the first file is loaded into memory as a - String. Before calling the getString method, the data - may have been in memory or on disk depending on its size. The second - file we assume it will be large and therefore never explicitly load - it into memory, though if it is less than 4096 bytes it will be - in memory before it is written to its final location. When writing to - the final location, if the data is larger than the - threshold, an attempt is made to rename the temporary file to - the given location. If it cannot be renamed, it is streamed to the - new location. -

- +

+ In the example above, the first file is loaded into memory as a + String. Before calling the getString method, + the data may have been in memory or on disk depending on its size. The + second file we assume it will be large and therefore never explicitly + load it into memory, though if it is less than 4096 bytes it will be + in memory before it is written to its final location. When writing to + the final location, if the data is larger than the threshold, an attempt + is made to rename the temporary file to the given location. If it cannot + be renamed, it is streamed to the new location. +

+

+ Please see the FileUpload + User Guide + for further details and examples of how to use this package. +

+ -- 2.11.0