/*
* Copyright 2004-2010 the Seasar Foundation and the Others.
*
* Licensed 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.slim3.controller.upload;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.servlet.http.HttpServletRequest;
import org.slim3.controller.upload.MultipartStream.ItemInputStream;
/**
* High level API for processing file uploads.
*
* @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
* @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
* @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
* @author <a href="mailto:jmcnally@collab.net">John McNally</a>
* @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
* @author Sean C. Sullivan
* @author higa
* @since 1.0.0
*
*/
public class FileUpload {
// ----------------------------------------------------- Manifest constants
/**
* HTTP content type header name.
*/
public static final String CONTENT_TYPE = "Content-type";
/**
* HTTP content disposition header name.
*/
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.
*/
public static final String FORM_DATA = "form-data";
/**
* Content-disposition value for file attachment.
*/
public static final String ATTACHMENT = "attachment";
/**
* Part of HTTP content type header.
*/
public static final String MULTIPART = "multipart/";
/**
* HTTP content type header for multipart forms.
*/
public static final String MULTIPART_FORM_DATA = "multipart/form-data";
/**
* HTTP content type header for multiple uploads.
*/
public static final String MULTIPART_MIXED = "multipart/mixed";
// ----------------------------------------------------------- Data members
/**
* 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;
/**
* <p>
* Utility method that determines whether the request contains multipart
* content.
* </p>
*
* <p>
* <strong>NOTE:</strong>This method will be moved to the
* <code>ServletFileUpload</code> 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.
* </p>
*
* @param request
* the request.
*
* @return <code>true</code> if the request is multipart; <code>false</code>
* otherwise.
*/
public static final boolean isMultipartContent(HttpServletRequest request) {
String contentType = request.getContentType();
if (contentType == null) {
return false;
}
if (contentType.toLowerCase().startsWith(MULTIPART)) {
return true;
}
return false;
}
/**
* Returns the maximum allowed size of a complete request, as opposed to
* {@link #getFileSizeMax()}.
*
* @return The maximum allowed size, in bytes. The default value of -1
* indicates, that there is no limit.
*
* @see #setSizeMax(long)
*
*/
public long getSizeMax() {
return sizeMax;
}
/**
* Sets the maximum allowed size of a complete request, as opposed to
* {@link #setFileSizeMax(long)}.
*
* @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) {
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 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.
*/
public String getHeaderEncoding() {
return headerEncoding;
}
/**
* 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.
*/
public void setHeaderEncoding(String encoding) {
headerEncoding = encoding;
}
/**
* Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
* compliant <code>multipart/form-data</code> stream.
*
* @param request
* The request.
*
* @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(HttpServletRequest request)
throws FileUploadException, IOException {
return new FileItemIteratorImpl(request);
}
/**
* Retrieves the boundary from the <code>Content-type</code> 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<String, String> params =
parser.parse(contentType, new char[] { ';', ',' });
String boundaryStr = 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 <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
*/
protected String getFileName(String pContentDisposition) {
String fileName = null;
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<String, String> params =
parser.parse(pContentDisposition, ';');
if (params.containsKey("filename")) {
fileName = 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;
}
/**
* 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>.
*/
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
*/
protected String getFieldName(String pContentDisposition) {
String fieldName = null;
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(pContentDisposition, ';');
fieldName = params.get("name");
if (fieldName != null) {
fieldName = fieldName.trim();
}
}
return fieldName;
}
/**
* <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.
*/
protected FileItemHeaders getParsedHeaders(String headerPart) {
final int len = headerPart.length();
FileItemHeaders headers = new FileItemHeaders();
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;
}
parseHeaderLine(headers, header);
}
return headers;
}
/**
* 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.
*/
protected 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.
*/
protected void parseHeaderLine(FileItemHeaders headers, String header) {
int colonOffset = header.indexOf(':');
if (colonOffset == -1) {
return;
}
String headerName = header.substring(0, colonOffset).trim();
String headerValue = header.substring(header.indexOf(':') + 1).trim();
headers.addHeader(headerName, headerValue);
}
/**
* The default implementation of {@link FileItemIterator}.
*/
protected class FileItemIteratorImpl implements FileItemIterator {
/**
* Default implementation of {@link FileItemStream}.
*/
protected 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) {
throw new SizeLimitExceededException("The field "
+ fieldName
+ " exceeds its maximum permitted "
+ " size of "
+ fileSizeMax
+ " characters.", pContentLength, fileSizeMax);
}
istream = new LimitedInputStream(istream, fileSizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
itemStream.close(true);
throw new SizeLimitExceededException("The field "
+ fieldName
+ " exceeds its maximum permitted "
+ " size of "
+ pSizeMax
+ " bytes.", pCount, pSizeMax);
}
};
}
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 getFileName() {
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 has bean already opened.");
}
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.
*/
protected MultipartStream multi;
/**
* The boundary, which separates the various parts.
*/
protected byte[] boundary;
/**
* The item, which we currently process.
*/
protected FileItemStreamImpl currentItem;
/**
* The current items field name.
*/
protected String currentFieldName;
/**
* Whether we are currently skipping the preamble.
*/
protected boolean skipPreamble;
/**
* Whether the current item may still be read.
*/
private boolean itemValid;
/**
* Whether we have seen the end of the file.
*/
protected 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(HttpServletRequest request)
throws FileUploadException, IOException {
if (request == null) {
throw new NullPointerException("The request parameter is null.");
}
String contentType = request.getContentType();
if ((contentType == null)
|| (!contentType.toLowerCase().startsWith(MULTIPART))) {
throw new IllegalStateException(
"The request doesn't contain a "
+ MULTIPART_FORM_DATA
+ " or "
+ MULTIPART_MIXED
+ " stream, content type header is "
+ contentType);
}
InputStream input = request.getInputStream();
if (input == null) {
eof = true;
return;
}
if (sizeMax >= 0) {
int requestSize = request.getContentLength();
if (requestSize == -1) {
input = new LimitedInputStream(input, sizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
throw new SizeLimitExceededException(
"the request was rejected because"
+ " its size ("
+ pCount
+ ") exceeds the configured maximum"
+ " ("
+ pSizeMax
+ ")",
pCount,
pSizeMax);
}
};
} 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 = request.getCharacterEncoding();
}
boundary = getBoundary(contentType);
if (boundary == null) {
throw new FileUploadException(
"The request was rejected because "
+ "no multipart boundary was found.");
}
multi = new MultipartStream(input, boundary);
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.
*/
protected 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));
itemValid = true;
return true;
}
} else {
String fileName = getFileName(headers);
if (fileName != null) {
currentItem =
new FileItemStreamImpl(
fileName,
currentFieldName,
headers.getHeader(CONTENT_TYPE),
false,
getContentLength(headers));
itemValid = true;
return true;
}
}
multi.discardBodyData();
}
}
/**
* Returns the content length.
*
* @param pHeaders
* the headers
* @return the content length
*/
protected 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;
}
}
}