You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by mg...@apache.org on 2010/12/05 14:08:00 UTC

svn commit: r1042345 [2/3] - in /wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util: io/ upload/

Modified: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadBase.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadBase.java?rev=1042345&r1=1042344&r2=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadBase.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadBase.java Sun Dec  5 13:08:00 2010
@@ -18,13 +18,18 @@ package org.apache.wicket.util.upload;
 
 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.wicket.util.io.Streams;
+import org.apache.wicket.util.upload.MultipartFormInputStream.ItemInputStream;
 
 /**
  * <p>
@@ -33,7 +38,10 @@ import java.util.Map;
  * 
  * <p>
  * This class handles multiple files per single HTML widget, sent using <code>multipart/mixed</code>
- * encoding type, as specified by <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
+ * encoding type, as specified by <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use
+ * {@link #parseRequest(HttpServletRequest)} to acquire a list of
+ * {@link org.apache.wicket.util.upload.FileItem}s associated with a given HTML widget.
+ * </p>
  * 
  * <p>
  * How the data for individual parts is stored is determined by the factory used to create them; a
@@ -83,6 +91,24 @@ public abstract class FileUploadBase
 		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 <code>true</code> if the request is multipart; <code>false</code> otherwise.
+	 * 
+	 * @deprecated Use the method on <code>ServletFileUpload</code> instead.
+	 */
+	@Deprecated
+	public static boolean isMultipartContent(HttpServletRequest req)
+	{
+		return ServletFileUpload.isMultipartContent(req);
+	}
+
+
 	// ----------------------------------------------------- Manifest constants
 
 
@@ -97,6 +123,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,7 +161,12 @@ 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}.
 	 */
+	@Deprecated
 	public static final int MAX_HEADER_SIZE = 1024;
 
 
@@ -138,16 +174,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,9 +216,11 @@ 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)
 	 * 
@@ -184,10 +232,12 @@ public abstract class FileUploadBase
 
 
 	/**
-	 * 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.
+	 *            The maximum allowed size, in bytes. The default value of -1 indicates, that there
+	 *            is no limit.
 	 * 
 	 * @see #getSizeMax()
 	 * 
@@ -197,10 +247,34 @@ public abstract class FileUploadBase
 		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 <code>null</code>, the platform default encoding is used.
+	 * not specified, or <code>null</code>, the request encoding is used. If that is also not
+	 * specified, or <code>null</code>, the platform default encoding is used.
 	 * 
 	 * @return The encoding used to read part headers.
 	 */
@@ -211,8 +285,9 @@ public abstract class FileUploadBase
 
 
 	/**
-	 * Specifies the character encoding to be used when reading the headers of individual parts.
-	 * When not specified, or <code>null</code>, the platform default encoding is used.
+	 * Specifies the character encoding to be used when reading the headers of individual part. When
+	 * not specified, or <code>null</code>, the request encoding is used. If that is also not
+	 * specified, or <code>null</code>, the platform default encoding is used.
 	 * 
 	 * @param encoding
 	 *            The encoding used to read part headers.
@@ -225,6 +300,50 @@ public abstract class FileUploadBase
 
 	// --------------------------------------------------------- Public methods
 
+
+	/**
+	 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a> compliant
+	 * <code>multipart/form-data</code> stream.
+	 * 
+	 * @param req
+	 *            The servlet request to be parsed.
+	 * 
+	 * @return A list of <code>FileItem</code> 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.
+	 * 
+	 * @deprecated Use the method in <code>ServletFileUpload</code> instead.
+	 */
+	@Deprecated
+	public List<FileItem> parseRequest(HttpServletRequest req) throws FileUploadException
+	{
+		return parseRequest(new ServletRequestContext(req));
+	}
+
+	/**
+	 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a> compliant
+	 * <code>multipart/form-data</code> stream.
+	 * 
+	 * @param ctx
+	 *            The context for the request to be parsed.
+	 * 
+	 * @return An iterator to instances of <code>FileItemStream</code> 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);
+	}
+
 	/**
 	 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a> compliant
 	 * <code>multipart/form-data</code> stream.
@@ -235,132 +354,55 @@ public abstract class FileUploadBase
 	 * @return A list of <code>FileItem</code> 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.
 	 */
 	public List<FileItem> parseRequest(final RequestContext ctx) throws FileUploadException
 	{
-		if (ctx == null)
-		{
-			throw new IllegalArgumentException("ctx parameter cannot be null");
-		}
-
-		List<FileItem> items = new ArrayList<FileItem>();
-		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);
-		}
-		int requestSize = ctx.getContentLength();
-
-		if ((requestSize == -1) && (getSizeMax() != Long.MAX_VALUE))
-		{
-			throw new UnknownSizeException("the request was rejected because its size is unknown");
-		}
-
-		if ((sizeMax >= 0) && (requestSize > sizeMax))
-		{
-			throw new SizeLimitExceededException("the request was rejected because "
-				+ "its size exceeds allowed range");
-		}
-
 		try
 		{
-			byte[] boundary = getBoundary(contentType);
-			if (boundary == null)
+			FileItemIterator iter = getItemIterator(ctx);
+			List<FileItem> items = new ArrayList<FileItem>();
+			FileItemFactory fac = getFileItemFactory();
+			if (fac == null)
 			{
-				throw new FileUploadException("the request was rejected because "
-					+ "no multipart boundary was found");
+				throw new NullPointerException("No FileItemFactory has been set.");
 			}
-
-			InputStream input = ctx.getInputStream();
-
-			MultipartFormInputStream multi = new MultipartFormInputStream(input, boundary);
-			multi.setHeaderEncoding(headerEncoding);
-
-			boolean nextPart = multi.skipPreamble();
-
-			// Don't allow a header larger than this size (to prevent DOS
-			// attacks)
-			final int maxHeaderBytes = 65536;
-			while (nextPart)
-			{
-				Map<String, String> headers = parseHeaders(multi.readHeaders(maxHeaderBytes));
-				String fieldName = getFieldName(headers);
-				if (fieldName != null)
-				{
-					String subContentType = getHeader(headers, CONTENT_TYPE);
-					if ((subContentType != null) &&
-						subContentType.toLowerCase().startsWith(MULTIPART_MIXED))
-					{
-						// Multiple files.
-						byte[] subBoundary = getBoundary(subContentType);
-						multi.setBoundary(subBoundary);
-						boolean nextSubPart = multi.skipPreamble();
-						while (nextSubPart)
-						{
-							headers = parseHeaders(multi.readHeaders(maxHeaderBytes));
-							if (getFileName(headers) != null)
-							{
-								FileItem item = createItem(headers, false);
-								items.add(item);
-								OutputStream os = item.getOutputStream();
-								try
-								{
-									multi.readBodyData(os);
-								}
-								finally
-								{
-									os.close();
-								}
-							}
-							else
-							{
-								// Ignore anything but files inside
-								// multipart/mixed.
-								multi.discardBodyData();
-							}
-							nextSubPart = multi.readBoundary();
-						}
-						multi.setBoundary(boundary);
-					}
-					else
-					{
-						FileItem item = createItem(headers, getFileName(headers) == null);
-						items.add(item);
-						OutputStream os = item.getOutputStream();
-						try
-						{
-							multi.readBodyData(os);
-						}
-						finally
-						{
-							os.close();
-						}
-					}
+			while (iter.hasNext())
+			{
+				FileItemStream item = iter.next();
+				FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
+					item.isFormField(), item.getName());
+				try
+				{
+					Streams.copyAndClose(item.openStream(), fileItem.getOutputStream());
 				}
-				else
+				catch (FileUploadIOException e)
+				{
+					throw (FileUploadException)e.getCause();
+				}
+				catch (IOException e)
+				{
+					throw new IOFileUploadException("Processing of " + MULTIPART_FORM_DATA +
+						" request failed. " + e.getMessage(), e);
+				}
+				if (fileItem instanceof FileItemHeadersSupport)
 				{
-					// Skip this part.
-					multi.discardBodyData();
+					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)
 		{
-			for (FileItem item : items)
-			{
-				item.delete();
-			}
-			throw new FileUploadException("Processing of " + MULTIPART_FORM_DATA +
-				" request failed. " + e.getMessage(), e);
+			throw new FileUploadException(e.getMessage(), e);
 		}
-
-		return items;
 	}
 
 
@@ -380,8 +422,8 @@ public abstract class FileUploadBase
 		ParameterParser parser = new ParameterParser();
 		parser.setLowerCaseNames(true);
 		// Parameter parser can handle null input
-		Map<String, String> params = parser.parse(contentType, ';');
-		String boundaryStr = params.get("boundary");
+		Map params = parser.parse(contentType, new char[] { ';', ',' });
+		String boundaryStr = (String)params.get("boundary");
 
 		if (boundaryStr == null)
 		{
@@ -407,39 +449,61 @@ public abstract class FileUploadBase
 	 *            A <code>Map</code> containing the HTTP request headers.
 	 * 
 	 * @return The file name for the current <code>encapsulation</code>.
+	 * @deprecated Use {@link #getFileName(FileItemHeaders)}.
 	 */
+	@Deprecated
 	protected String getFileName(final Map<String, String> headers)
 	{
+		return getFileName(getHeader(headers, CONTENT_DISPOSITION));
+	}
+
+	/**
+	 * Retrieves the file name from the <code>Content-disposition</code> header.
+	 * 
+	 * @param headers
+	 *            The HTTP headers object.
+	 * 
+	 * @return The file name for the current <code>encapsulation</code>.
+	 */
+	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))
+		if (pContentDisposition != null)
 		{
-			ParameterParser parser = new ParameterParser();
-			parser.setLowerCaseNames(true);
-			// Parameter parser can handle null input
-			Map<String, String> params = parser.parse(cd, ';');
-			if (params.containsKey("filename"))
+			String cdl = pContentDisposition.toLowerCase();
+			if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT))
 			{
-				fileName = params.get("filename");
-				if (fileName != null)
+				ParameterParser parser = new ParameterParser();
+				parser.setLowerCaseNames(true);
+				// Parameter parser can handle null input
+				Map params = parser.parse(pContentDisposition, ';');
+				if (params.containsKey("filename"))
 				{
-					fileName = fileName.trim();
-					int index = fileName.lastIndexOf('\\');
-					if (index == -1)
+					fileName = (String)params.get("filename");
+					if (fileName != null)
 					{
-						index = fileName.lastIndexOf('/');
+						fileName = fileName.trim();
 					}
-					if (index != -1)
+					else
 					{
-						fileName = fileName.substring(index + 1);
+						// Even if there is no value, the parameter is present,
+						// so we return an empty file name rather than no file
+						// name.
+						fileName = "";
 					}
 				}
-				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;
@@ -454,18 +518,28 @@ public abstract class FileUploadBase
 	 * 
 	 * @return The field name for the current <code>encapsulation</code>.
 	 */
-	protected String getFieldName(final Map<String, String> headers)
+	protected String getFieldName(final 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))
+		if (pContentDisposition != null && pContentDisposition.toLowerCase().startsWith(FORM_DATA))
 		{
-
 			ParameterParser parser = new ParameterParser();
 			parser.setLowerCaseNames(true);
 			// Parameter parser can handle null input
-			Map<String, String> params = parser.parse(cd, ';');
-			fieldName = params.get("name");
+			Map params = parser.parse(pContentDisposition, ';');
+			fieldName = (String)params.get("name");
 			if (fieldName != null)
 			{
 				fieldName = fieldName.trim();
@@ -474,6 +548,21 @@ public abstract class FileUploadBase
 		return fieldName;
 	}
 
+	/**
+	 * Retrieves the field name from the <code>Content-disposition</code> header.
+	 * 
+	 * @param headers
+	 *            A <code>Map</code> containing the HTTP request headers.
+	 * 
+	 * @return The field name for the current <code>encapsulation</code>.
+	 * @deprecated Use {@link #getFieldName(FileItemHeaders)}.
+	 */
+	@Deprecated
+	protected String getFieldName(Map<String, String> headers)
+	{
+		return getFieldName(getHeader(headers, CONTENT_DISPOSITION));
+	}
+
 
 	/**
 	 * Creates a new {@link FileItem} instance.
@@ -484,14 +573,20 @@ public abstract class FileUploadBase
 	 *            Whether or not this item is a form field, as opposed to a file.
 	 * 
 	 * @return A newly created <code>FileItem</code> instance.
+	 * 
+	 * @throws FileUploadException
+	 *             if an error occurs.
+	 * @deprecated This method is no longer used in favour of internally created instances of
+	 *             {@link FileItem}.
 	 */
+	@Deprecated
 	protected FileItem createItem(final Map<String, String> headers, final boolean isFormField)
+		throws FileUploadException
 	{
 		return getFileItemFactory().createItem(getFieldName(headers),
 			getHeader(headers, CONTENT_TYPE), isFormField, getFileName(headers));
 	}
 
-
 	/**
 	 * <p>
 	 * Parses the <code>header-part</code> and returns as key/value pairs.
@@ -505,60 +600,137 @@ public abstract class FileUploadBase
 	 * 
 	 * @return A <code>Map</code> containing the parsed HTTP request headers.
 	 */
-	protected Map<String, String> parseHeaders(final String headerPart)
+	protected FileItemHeaders getParsedHeaders(final String headerPart)
 	{
-		Map<String, String> headers = new HashMap<String, String>();
-		char[] buffer = new char[MAX_HEADER_SIZE];
-		boolean done = false;
-		int j = 0;
-		int i;
-		String header, headerName, headerValue;
-		try
+		final int len = headerPart.length();
+		FileItemHeadersImpl headers = newFileItemHeaders();
+		int start = 0;
+		for (;;)
 		{
-			while (!done)
+			int end = parseEndOfLine(headerPart, start);
+			if (start == end)
 			{
-				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
+				break;
+			}
+			String header = headerPart.substring(start, end);
+			start = end + 2;
+			while (start < len)
+			{
+				int nonWs = start;
+				while (nonWs < len)
 				{
-					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 header of that name exists,
-						// append to the list.
-						headers.put(headerName, getHeader(headers, headerName) + ',' + headerValue);
-					}
-					else
+					char c = headerPart.charAt(nonWs);
+					if (c != ' ' && c != '\t')
 					{
-						headers.put(headerName, headerValue);
+						break;
 					}
+					++nonWs;
 				}
+				if (nonWs == start)
+				{
+					break;
+				}
+				// Continuation line found
+				end = parseEndOfLine(headerPart, nonWs);
+				header += " " + headerPart.substring(nonWs, end);
+				start = end + 2;
 			}
+			parseHeaderLine(headers, header);
 		}
-		catch (IndexOutOfBoundsException e)
+		return headers;
+	}
+
+	/**
+	 * Creates a new instance of {@link FileItemHeaders}.
+	 * 
+	 * @return The new instance.
+	 */
+	protected FileItemHeadersImpl newFileItemHeaders()
+	{
+		return new FileItemHeadersImpl();
+	}
+
+	/**
+	 * <p>
+	 * Parses the <code>header-part</code> and returns as key/value pairs.
+	 * 
+	 * <p>
+	 * If there are multiple headers of the same names, the name will map to a comma-separated list
+	 * containing the values.
+	 * 
+	 * @param headerPart
+	 *            The <code>header-part</code> of the current <code>encapsulation</code>.
+	 * 
+	 * @return A <code>Map</code> containing the parsed HTTP request headers.
+	 * @deprecated Use {@link #getParsedHeaders(String)}
+	 */
+	@Deprecated
+	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 (;;)
 		{
-			// Headers were malformed. continue with all that was
-			// parsed.
+			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;
 		}
-		return headers;
 	}
 
+	/**
+	 * 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 header lookup is
@@ -571,27 +743,506 @@ 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)}.
 	 */
+	@Deprecated
 	protected final String getHeader(final Map<String, String> headers, final String name)
 	{
 		return 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)
+					{
+						@Override
+						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 MultipartFormInputStream multi;
+		/**
+		 * The notifier, which used for triggering the {@link ProgressListener}.
+		 */
+		private final MultipartFormInputStream.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)
+					{
+						@Override
+						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 MultipartFormInputStream.ProgressNotifier(listener,
+				ctx.getContentLength());
+			multi = new MultipartFormInputStream(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 <code>FileUploadIOException</code> 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.
+		 */
+		@Override
+		public Throwable getCause()
+		{
+			return cause;
+		}
+	}
 
 	/**
 	 * Thrown to indicate that the request is not a multipart request.
 	 */
 	public static class InvalidContentTypeException extends FileUploadException
 	{
-
-		private static final long serialVersionUID = 1L;
+		/**
+		 * The exceptions UID, for serializing an instance.
+		 */
+		private static final long serialVersionUID = -9073026332015646668L;
 
 		/**
 		 * Constructs a <code>InvalidContentTypeException</code> with no detail message.
 		 */
 		public InvalidContentTypeException()
 		{
-			super();
+			// Nothing to do.
 		}
 
 		/**
@@ -606,14 +1257,114 @@ public abstract class FileUploadBase
 		}
 	}
 
+	/**
+	 * 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.
+		 */
+		@Override
+		public Throwable getCause()
+		{
+			return cause;
+		}
+	}
 
 	/**
-	 * Thrown to indicate that the request size is not specified.
+	 * This exception is thrown, if a requests permitted size is exceeded.
 	 */
-	public static class UnknownSizeException extends FileUploadException
+	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;
+		}
+	}
 
-		private static final long serialVersionUID = 1L;
+	/**
+	 * 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.
+	 */
+	@Deprecated
+	public static class UnknownSizeException extends FileUploadException
+	{
+		/**
+		 * The exceptions UID, for serializing an instance.
+		 */
+		private static final long serialVersionUID = 7062279004812015273L;
 
 		/**
 		 * Constructs a <code>UnknownSizeException</code> with no detail message.
@@ -635,33 +1386,98 @@ public abstract class FileUploadBase
 		}
 	}
 
-
 	/**
 	 * Thrown to indicate that the request size exceeds the configured maximum.
 	 */
-	public static class SizeLimitExceededException extends FileUploadException
+	public static class SizeLimitExceededException extends SizeException
 	{
-
-		private static final long serialVersionUID = 1L;
+		/**
+		 * The exceptions UID, for serializing an instance.
+		 */
+		private static final long serialVersionUID = -2474893167098052828L;
 
 		/**
-		 * Constructs a <code>SizeExceededException</code> with no detail message.
+		 * @deprecated Replaced by {@link #SizeLimitExceededException(String, long, long)}
 		 */
+		@Deprecated
 		public SizeLimitExceededException()
 		{
-			super();
+			this(null, 0, 0);
+		}
+
+		/**
+		 * @deprecated Replaced by {@link #SizeLimitExceededException(String, long, long)}
+		 * @param message
+		 *            The exceptions detail message.
+		 */
+		@Deprecated
+		public SizeLimitExceededException(String message)
+		{
+			this(message, 0, 0);
 		}
 
 		/**
-		 * Constructs an <code>SizeExceededException</code> with the specified detail message.
+		 * Constructs a <code>SizeExceededException</code> 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 SizeLimitExceededException(final String message)
+		public SizeLimitExceededException(String message, long actual, long permitted)
 		{
-			super(message);
+			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 <code>SizeExceededException</code> 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;
+	}
 }

Modified: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadException.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadException.java?rev=1042345&r1=1042344&r2=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadException.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/FileUploadException.java Sun Dec  5 13:08:00 2010
@@ -16,22 +16,33 @@
  */
 package org.apache.wicket.util.upload;
 
-import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
 
 /**
  * Exception for errors encountered while processing the request.
  * 
  * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
  */
-public class FileUploadException extends IOException
+public class FileUploadException extends Exception
 {
-	private static final long serialVersionUID = 1L;
+	/**
+	 * 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 <code>FileUploadException</code> without message.
 	 */
 	public FileUploadException()
 	{
+		this(null, null);
 	}
 
 	/**
@@ -42,32 +53,60 @@ public class FileUploadException extends
 	 */
 	public FileUploadException(final String msg)
 	{
-		super(msg);
+		this(msg, null);
 	}
 
 	/**
-	 * Constructs a new <code>FileUploadException</code> with specified cause.
+	 * Creates a new <code>FileUploadException</code> with the given detail message and cause.
 	 * 
+	 * @param msg
+	 *            The exceptions detail message.
 	 * @param cause
-	 *            the cause.
+	 *            The exceptions cause.
 	 */
-	public FileUploadException(final Throwable cause)
+	public FileUploadException(String msg, Throwable cause)
 	{
-		super();
-		initCause(cause);
+		super(msg);
+		this.cause = cause;
 	}
 
 	/**
-	 * Constructs a new <code>FileUploadException</code> with specified detail message and cause
+	 * Prints this throwable and its backtrace to the specified print stream.
 	 * 
-	 * @param message
-	 *            the error message.
-	 * @param cause
-	 *            the cause.
+	 * @param stream
+	 *            <code>PrintStream</code> to use for output
 	 */
-	public FileUploadException(final String message, final Throwable cause)
+	@Override
+	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
+	 *            <code>PrintWriter</code> to use for output
+	 */
+	@Override
+	public void printStackTrace(PrintWriter writer)
+	{
+		super.printStackTrace(writer);
+		if (cause != null)
+		{
+			writer.println("Caused by:");
+			cause.printStackTrace(writer);
+		}
+	}
+
+	@Override
+	public Throwable getCause()
 	{
-		super(message);
-		initCause(cause);
+		return cause;
 	}
 }

Added: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/LimitedInputStream.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/LimitedInputStream.java?rev=1042345&view=auto
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/LimitedInputStream.java (added)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/LimitedInputStream.java Sun Dec  5 13:08:00 2010
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.util.upload;
+
+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 final 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
+	 * <code>int</code> in the range <code>0</code> to <code>255</code>. If no byte is available
+	 * because the end of the stream has been reached, the value <code>-1</code> is returned. This
+	 * method blocks until input data is available, the end of the stream is detected, or an
+	 * exception is thrown.
+	 * <p>
+	 * This method simply performs <code>in.read()</code> and returns the result.
+	 * 
+	 * @return the next byte of data, or <code>-1</code> if the end of the stream is reached.
+	 * @exception IOException
+	 *                if an I/O error occurs.
+	 * @see java.io.FilterInputStream#in
+	 */
+	@Override
+	public int read() throws IOException
+	{
+		int res = super.read();
+		if (res != -1)
+		{
+			count++;
+			checkLimit();
+		}
+		return res;
+	}
+
+	/**
+	 * Reads up to <code>len</code> bytes of data from this input stream into an array of bytes. If
+	 * <code>len</code> is not zero, the method blocks until some input is available; otherwise, no
+	 * bytes are read and <code>0</code> is returned.
+	 * <p>
+	 * This method simply performs <code>in.read(b, off, len)</code> and returns the result.
+	 * 
+	 * @param b
+	 *            the buffer into which the data is read.
+	 * @param off
+	 *            The start offset in the destination array <code>b</code>.
+	 * @param len
+	 *            the maximum number of bytes read.
+	 * @return the total number of bytes read into the buffer, or <code>-1</code> if there is no
+	 *         more data because the end of the stream has been reached.
+	 * @exception NullPointerException
+	 *                If <code>b</code> is <code>null</code>.
+	 * @exception IndexOutOfBoundsException
+	 *                If <code>off</code> is negative, <code>len</code> is negative, or
+	 *                <code>len</code> is greater than <code>b.length - off</code>
+	 * @exception IOException
+	 *                if an I/O error occurs.
+	 * @see java.io.FilterInputStream#in
+	 */
+	@Override
+	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 <code>in.close()</code>.
+	 * 
+	 * @exception IOException
+	 *                if an I/O error occurs.
+	 * @see java.io.FilterInputStream#in
+	 */
+	@Override
+	public void close() throws IOException
+	{
+		closed = true;
+		super.close();
+	}
+}

Propchange: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/LimitedInputStream.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/MultipartFormInputStream.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/MultipartFormInputStream.java?rev=1042345&r1=1042344&r2=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/MultipartFormInputStream.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/MultipartFormInputStream.java Sun Dec  5 13:08:00 2010
@@ -22,6 +22,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 
+import org.apache.wicket.util.io.Streams;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,7 +42,7 @@ import org.slf4j.LoggerFactory;
  *   multipart-body := preamble 1*encapsulation close-delimiter epilogue<br>
  *   encapsulation := delimiter body CRLF<br>
  *   delimiter := "--" boundary CRLF<br>
- *   close-delimiter := "--" boundary "--"<br>
+ *   close-delimiter := "--" boudary "--"<br>
  *   preamble := &lt;ignore&gt;<br>
  *   epilogue := &lt;ignore&gt;<br>
  *   body := header-part CRLF body-part<br>
@@ -53,8 +54,8 @@ import org.slf4j.LoggerFactory;
  * </code>
  * 
  * <p>
- * Note that body-data can contain another multipart entity. There is limited support for single
- * pass processing of such nested streams. The nested stream is <strong>required</strong> to have a
+ * Note that body-data can contain another mulipart entity. There is limited support for single pass
+ * processing of such nested streams. The nested stream is <strong>required</strong> to have a
  * boundary token of the same length as the parent stream (see {@link #setBoundary(byte[])}).
  * 
  * <p>
@@ -64,7 +65,7 @@ import org.slf4j.LoggerFactory;
  *      try {
  *          MultipartStream multipartStream = new MultipartStream(input,
  *                                                                boundary);
- *          boolean nextPart = malitPartStream.skipPreamble();
+ *        boolean nextPart = multipartStream.skipPreamble();
  *          OutputStream output;
  *          while(nextPart) {
  *              header = chunks.readHeader();
@@ -78,96 +79,189 @@ import org.slf4j.LoggerFactory;
  *      } catch(IOException) {
  *            // a read or write error occurred
  *      }
+ * 
  * </pre>
  * 
  * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
  * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
  * @author Sean C. Sullivan
  * 
- * @version $Id$
  */
 public class MultipartFormInputStream
 {
 	/** Log. */
 	private static final Logger log = LoggerFactory.getLogger(MultipartFormInputStream.class);
 
+	/**
+	 * 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 <code>header-part</code> that will be processed (10 kilobytes = 10240
 	 * bytes.).
 	 */
 	public static final int HEADER_PART_SIZE_MAX = 10240;
 
+
 	/**
 	 * The default length of the buffer used for processing a request.
 	 */
 	protected static final int DEFAULT_BUFSIZE = 4096;
 
+
 	/**
 	 * A byte sequence that marks the end of <code>header-part</code> (<code>CRLFCRLF</code>).
 	 */
 	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 (
 	 * <code>CRLF</code>).
 	 */
 	protected static final byte[] FIELD_SEPARATOR = { CR, LF };
 
+
 	/**
 	 * A byte sequence that that follows a delimiter of the last encapsulation in the stream (
 	 * <code>--</code>).
 	 */
 	protected static final byte[] STREAM_TERMINATOR = { DASH, DASH };
 
+
+	/**
+	 * A byte sequence that precedes a boundary (<code>CRLF--</code>).
+	 */
+	protected static final byte[] BOUNDARY_PREFIX = { CR, LF, DASH, DASH };
+
+
 	// ----------------------------------------------------------- Data members
 
+
 	/**
 	 * The input stream from which data is read.
 	 */
-	private InputStream input;
+	private final InputStream input;
+
 
 	/**
 	 * The length of the boundary token plus the leading <code>CRLF--</code>.
 	 */
 	private int boundaryLength;
 
+
 	/**
 	 * The amount of data, in bytes, that must be kept in the buffer in order to detect delimiters
 	 * reliably.
 	 */
-	private int keepRegion;
+	private final int keepRegion;
+
 
 	/**
 	 * The byte sequence that partitions the stream.
 	 */
-	private byte[] boundary;
+	private final byte[] boundary;
+
 
 	/**
 	 * 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;
+
 
 	/**
 	 * The index of first valid character in the buffer. <br>
@@ -175,33 +269,44 @@ public class MultipartFormInputStream
 	 */
 	private int head;
 
+
 	/**
-	 * The index of last valid character in the buffer + 1. <br>
+	 * The index of last valid characer in the buffer + 1. <br>
 	 * 0 <= tail <= bufSize
 	 */
 	private int tail;
 
+
 	/**
 	 * The content encoding to use when reading headers.
 	 */
 	private String headerEncoding;
 
+
+	/**
+	 * The progress notifier, if any, or null.
+	 */
+	private final ProgressNotifier notifier;
+
 	// ----------------------------------------------------------- Constructors
 
 	/**
-	 * Default constructor.
-	 * 
-	 * @see #MultipartFormInputStream(InputStream, byte[], int)
-	 * @see #MultipartFormInputStream(InputStream, byte[])
+	 * Creates a new instance.
 	 * 
+	 * @deprecated Use
+	 *             {@link #MultipartStream(InputStream, byte[], org.apache.wicket.util.uploadMultipartStream.ProgressNotifier)}
+	 *             , or
+	 *             {@link #MultipartStream(InputStream, byte[], int, org.apache.wicket.util.uploadMultipartStream.ProgressNotifier)}
 	 */
+	@Deprecated
 	public MultipartFormInputStream()
 	{
+		this(null, null, null);
 	}
 
 	/**
 	 * <p>
-	 * Constructs a <code>MultipartStream</code> with a custom size buffer.
+	 * Constructs a <code>MultipartStream</code> with a custom size buffer and no progress notifier.
 	 * 
 	 * <p>
 	 * Note that the buffer must be at least big enough to contain the boundary string, plus 4
@@ -215,28 +320,54 @@ public class MultipartFormInputStream
 	 * @param bufSize
 	 *            The size of the buffer to be used, in bytes.
 	 * 
+	 * @see #MultipartFormInputStream(InputStream, byte[],
+	 *      MultipartFormInputStream.ProgressNotifier)
+	 * @deprecated Use
+	 *             {@link #MultipartStream(InputStream, byte[], int, org.apache.wicket.util.uploadMultipartStream.ProgressNotifier)}
+	 *             .
+	 */
+	@Deprecated
+	public MultipartFormInputStream(InputStream input, byte[] boundary, int bufSize)
+	{
+		this(input, boundary, bufSize, null);
+	}
+
+	/**
+	 * <p>
+	 * Constructs a <code>MultipartStream</code> with a custom size buffer.
+	 * 
+	 * <p>
+	 * 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.
 	 * 
-	 * @see #MultipartFormInputStream()
-	 * @see #MultipartFormInputStream(InputStream, byte[])
+	 * @param input
+	 *            The <code>InputStream</code> to serve as a data source.
+	 * @param boundary
+	 *            The token used for dividing the stream into <code>encapsulations</code>.
+	 * @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 #MultipartFormInputStream(InputStream, byte[],
+	 *      MultipartFormInputStream.ProgressNotifier)
 	 */
-	public MultipartFormInputStream(final InputStream input, final byte[] boundary,
-		final int bufSize)
+	MultipartFormInputStream(InputStream input, byte[] boundary, int bufSize,
+		ProgressNotifier pNotifier)
 	{
 		this.input = input;
 		this.bufSize = bufSize;
 		buffer = new byte[bufSize];
+		notifier = pNotifier;
 
-		// We prepend CR/LF to the boundary to chop trailing CR/LF from
+		// We prepend CR/LF to the boundary to chop trailng CR/LF from
 		// body-data tokens.
-		this.boundary = new byte[boundary.length + 4];
-		boundaryLength = boundary.length + 4;
-		keepRegion = boundary.length + 3;
-		this.boundary[0] = CR;
-		this.boundary[1] = LF;
-		this.boundary[2] = DASH;
-		this.boundary[3] = DASH;
-		System.arraycopy(boundary, 0, this.boundary, 4, boundary.length);
+		this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
+		boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
+		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;
@@ -251,17 +382,18 @@ public class MultipartFormInputStream
 	 *            The <code>InputStream</code> to serve as a data source.
 	 * @param boundary
 	 *            The token used for dividing the stream into <code>encapsulations</code>.
+	 * @param pNotifier
+	 *            An object for calling the progress listener, if any.
 	 * 
-	 * @see #MultipartFormInputStream()
-	 * @see #MultipartFormInputStream(InputStream, byte[], int)
 	 * 
+	 * @see #MultipartFormInputStream(InputStream, byte[], int,
+	 *      MultipartFormInputStream.ProgressNotifier)
 	 */
-	public MultipartFormInputStream(final InputStream input, final byte[] boundary)
+	MultipartFormInputStream(InputStream input, byte[] boundary, ProgressNotifier pNotifier)
 	{
-		this(input, boundary, DEFAULT_BUFSIZE);
+		this(input, boundary, DEFAULT_BUFSIZE, pNotifier);
 	}
 
-
 	// --------------------------------------------------------- Public methods
 
 
@@ -296,8 +428,8 @@ public class MultipartFormInputStream
 	 * 
 	 * @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
 	{
@@ -312,6 +444,10 @@ public class MultipartFormInputStream
 				// No more data available.
 				throw new IOException("No more data is available");
 			}
+			if (notifier != null)
+			{
+				notifier.noteBytesRead(tail);
+			}
 		}
 		return buffer[head++];
 	}
@@ -324,8 +460,8 @@ public class MultipartFormInputStream
 	 * @return <code>true</code> if there are more encapsulations in this stream; <code>false</code>
 	 *         otherwise.
 	 * 
-	 * @exception MalformedStreamException
-	 *                if the stream ends unexpectedly or fails to follow required syntax.
+	 * @throws MalformedStreamException
+	 *             if the stream ends unexpecetedly or fails to follow required syntax.
 	 */
 	public boolean readBoundary() throws MalformedStreamException
 	{
@@ -387,59 +523,59 @@ public class MultipartFormInputStream
 	 * @param boundary
 	 *            The boundary to be used for parsing of the nested stream.
 	 * 
-	 * @exception IllegalBoundaryException
-	 *                if the <code>boundary</code> has a different length than the one being
-	 *                currently parsed.
+	 * @throws IllegalBoundaryException
+	 *             if the <code>boundary</code> has a different length than the one being currently
+	 *             parsed.
 	 */
 	public void setBoundary(final byte[] boundary) throws IllegalBoundaryException
 	{
-		if (boundary.length != boundaryLength - 4)
+		if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length)
 		{
 			throw new IllegalBoundaryException("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);
 	}
 
+
 	/**
 	 * <p>
 	 * Reads the <code>header-part</code> of the current <code>encapsulation</code>.
+	 * 
 	 * <p>
 	 * Headers are returned verbatim to the input stream, including the trailing <code>CRLF</code>
 	 * marker. Parsing is left to the application.
 	 * 
-	 * @param maxSize
-	 *            The maximum amount to read before giving up
+	 * <p>
+	 * <strong>TODO</strong> allow limiting maximum header size to protect against abuse.
 	 * 
 	 * @return The <code>header-part</code> of the current encapsulation.
 	 * 
-	 * @exception MalformedStreamException
-	 *                if the stream ends unexpectedly.
+	 * @throws MalformedStreamException
+	 *             if the stream ends unexpecetedly.
 	 */
-	public String readHeaders(final int maxSize) throws MalformedStreamException
+	public String readHeaders() 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)
+		while (i < HEADER_SEPARATOR.length)
 		{
 			try
 			{
-				b[0] = readByte();
+				b = readByte();
 			}
 			catch (IOException e)
 			{
 				throw new MalformedStreamException("Stream ended unexpectedly");
 			}
-			size++;
-			if (size > maxSize)
+			if (++size > HEADER_PART_SIZE_MAX)
 			{
-				throw new MalformedStreamException("Stream exceeded maximum of " + maxSize +
-					" bytes");
+				throw new MalformedStreamException("Header section has more than " +
+					HEADER_PART_SIZE_MAX + " bytes (maybe it is not properly terminated)");
 			}
-			if (b[0] == HEADER_SEPARATOR[i])
+			if (b == HEADER_SEPARATOR[i])
 			{
 				i++;
 			}
@@ -447,10 +583,7 @@ public class MultipartFormInputStream
 			{
 				i = 0;
 			}
-			if (size <= sizeMax)
-			{
-				baos.write(b[0]);
-			}
+			baos.write(b);
 		}
 
 		String headers = null;
@@ -483,81 +616,36 @@ public class MultipartFormInputStream
 	 * 
 	 * <p>
 	 * Arbitrary large amounts of data can be processed by this method using a constant size buffer.
-	 * (see {@link #MultipartFormInputStream(InputStream,byte[],int) constructor}).
+	 * (see
+	 * {@link #MultipartFormInputStream(InputStream,byte[],int, MultipartFormInputStream.ProgressNotifier)
+	 * constructor}).
 	 * 
 	 * @param output
-	 *            The <code>Stream</code> to write data into.
+	 *            The <code>Stream</code> 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(final OutputStream output) throws MalformedStreamException, IOException
 	{
-		boolean done = false;
-		int pad;
-		int pos;
-		int bytesRead;
-		int total = 0;
-		while (!done)
-		{
-			// Is boundary token present somewhere 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 beginning 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;
+		final InputStream istream = newInputStream();
+		return Streams.copy(istream, output == null ? new NoopOutputStream() : output);
 	}
 
+	/**
+	 * Creates a new {@link ItemInputStream}.
+	 * 
+	 * @return A new instance of {@link ItemInputStream}.
+	 */
+	ItemInputStream newInputStream()
+	{
+		return new ItemInputStream();
+	}
 
 	/**
 	 * <p>
@@ -568,75 +656,24 @@ public class MultipartFormInputStream
 	 * 
 	 * @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 somewhere 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 beginning 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;
+		return readBodyData(null);
 	}
 
+
 	/**
 	 * Finds the beginning of the first <code>encapsulation</code>.
 	 * 
 	 * @return <code>true</code> if an <code>encapsulation</code> 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
 	{
@@ -648,7 +685,7 @@ public class MultipartFormInputStream
 			// Discard all data up to the delimiter.
 			discardBodyData();
 
-			// Read boundary - if succeeded, the stream contains an
+			// Read boundary - if succeded, the stream contains an
 			// encapsulation.
 			return readBoundary();
 		}
@@ -783,6 +820,7 @@ public class MultipartFormInputStream
 		return sbTemp.toString();
 	}
 
+
 	/**
 	 * Thrown to indicate that the input stream fails to follow the required syntax.
 	 */
@@ -838,23 +876,342 @@ public class MultipartFormInputStream
 		}
 	}
 
+	/**
+	 * 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 = MultipartFormInputStream.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.
+		 */
+		@Override
+		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.
+		 */
+		@Override
+		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.
+		 */
+		@Override
+		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.
+		 */
+		@Override
+		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.
+		 */
+		@Override
+		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;
+		}
+	}
+
+	private final static class NoopOutputStream extends OutputStream
+	{
+		@Override
+		public void close()
+		{
+		}
+
+		@Override
+		public void flush()
+		{
+		}
+
+		@Override
+		public void write(byte[] b)
+		{
+		}
+
+		@Override
+		public void write(byte[] b, int i, int l)
+		{
+		}
+
+		@Override
+		public void write(int b)
+		{
+		}
+	}
 
 	// ------------------------------------------------------ Debugging methods
 
 
 	// These are the methods that were used to debug this stuff.
 	/*
+	 * 
 	 * // Dump data. protected void dump() { System.out.println("01234567890"); byte[] temp = new
 	 * byte[buffer.length]; for(int i=0; i<buffer.length; i++) { if (buffer[i] == 0x0D || buffer[i]
 	 * == 0x0A) { temp[i] = 0x21; } else { temp[i] = buffer[i]; } } System.out.println(new
 	 * String(temp)); int i; for (i=0; i<head; i++) System.out.print(" "); System.out.println("h");
 	 * for (i=0; i<tail; i++) System.out.print(" "); System.out.println("t"); System.out.flush(); }
+	 * 
 	 * // Main routine, for testing purposes only. // // @param args A String[] with the command
-	 * line arguments. // @exception Exception, a generic exception. public static void main(
-	 * String[] args ) throws Exception { File boundaryFile = new File("boundary.dat"); int
-	 * boundarySize = (int)boundaryFile.length(); byte[] boundary = new byte[boundarySize];
-	 * FileInputStream input = new FileInputStream(boundaryFile);
-	 * input.read(boundary,0,boundarySize);
+	 * line arguments. // @throws Exception, a generic exception. public static void main( String[]
+	 * args ) throws Exception { File boundaryFile = new File("boundary.dat"); int boundarySize =
+	 * (int)boundaryFile.length(); byte[] boundary = new byte[boundarySize]; FileInputStream input =
+	 * new FileInputStream(boundaryFile); input.read(boundary,0,boundarySize);
 	 * 
 	 * input = new FileInputStream("multipart.dat"); MultipartStream chunks = new
 	 * MultipartStream(input, boundary);

Modified: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ParameterParser.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ParameterParser.java?rev=1042345&r1=1042344&r2=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ParameterParser.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ParameterParser.java Sun Dec  5 13:08:00 2010
@@ -20,8 +20,8 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * A simple parser intended to parse sequences of name/value pairs. Parameter values are expected to
- * be enclosed in quotes if they contain unsafe characters, such as '=' characters or separators.
+ * 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.
  * 
  * <p>
@@ -122,7 +122,7 @@ public class ParameterParser
 	 * Tests if the given character is present in the array of characters.
 	 * 
 	 * @param ch
-	 *            the character to test for presence in the array of characters
+	 *            the character to test for presense in the array of characters
 	 * @param charray
 	 *            the array of characters to test against
 	 * 
@@ -233,6 +233,43 @@ public class ParameterParser
 
 	/**
 	 * 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<String, String> parse(final String str, char[] separators)
+	{
+		if (separators == null || separators.length == 0)
+		{
+			return new HashMap<String, String>();
+		}
+		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

Copied: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ProgressListener.java (from r1042295, wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java)
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ProgressListener.java?p2=wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ProgressListener.java&p1=wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java&r1=1042295&r2=1042345&rev=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/ProgressListener.java Sun Dec  5 13:08:00 2010
@@ -16,42 +16,23 @@
  */
 package org.apache.wicket.util.upload;
 
-import java.io.IOException;
-import java.io.InputStream;
 
 /**
- * <p>
- * Abstracts access to the request information needed for file uploads. This interface should be
- * implemented for each type of request that may be handled by FileUpload, such as servlets and
- * portlets.
- * </p>
- * 
- * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
+ * The {@link ProgressListener} may be used to display a progress bar or do stuff like that.
  */
-public interface RequestContext
+public interface ProgressListener
 {
-
-	/**
-	 * 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.
+	 * Updates the listeners status information.
 	 * 
-	 * @throws IOException
-	 *             if a problem occurs.
+	 * @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, ...)
 	 */
-	InputStream getInputStream() throws IOException;
+	void update(long pBytesRead, long pContentLength, int pItems);
 }

Modified: wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java?rev=1042345&r1=1042344&r2=1042345&view=diff
==============================================================================
--- wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java (original)
+++ wicket/trunk/wicket-util/src/main/java/org/apache/wicket/util/upload/RequestContext.java Sun Dec  5 13:08:00 2010
@@ -21,7 +21,7 @@ import java.io.InputStream;
 
 /**
  * <p>
- * Abstracts access to the request information needed for file uploads. This interface should be
+ * 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.
  * </p>
@@ -32,6 +32,13 @@ 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.