MultipartStreampublic class MultipartStream extends Object Low level API for processing file uploads.
This class can be used to process data streams conforming to MIME
'multipart' format as defined in
RFC 1867. Arbitrarily
large amounts of data in the stream can be processed under constant
memory usage.
The format of the stream is defined in the following way:
multipart-body := preamble 1*encapsulation close-delimiter epilogue
encapsulation := delimiter body CRLF
delimiter := "--" boundary CRLF
close-delimiter := "--" boudary "--"
preamble := <ignore>
epilogue := <ignore>
body := header-part CRLF body-part
header-part := 1*header CRLF
header := header-name ":" header-value
header-name := <printable ascii characters except ":">
header-value := <any ascii characters except CR & LF>
body-data := <arbitrary data>
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 required to have a
boundary token of the same length as the parent stream (see {@link
#setBoundary(byte[])}).
Here is an exaple of usage of this class.
try {
MultipartStream multipartStream = new MultipartStream(input,
boundary);
boolean nextPart = malitPartStream.skipPreamble();
OutputStream output;
while(nextPart) {
header = chunks.readHeader();
// process headers
// create some output stream
multipartStream.readBodyPart(output);
nextPart = multipartStream.readBoundary();
}
} catch(MultipartStream.MalformedStreamException e) {
// the stream failed to follow required syntax
} catch(IOException) {
// a read or write error occurred
}
|
Fields Summary |
---|
public static final int | HEADER_PART_SIZE_MAXThe maximum length of header-part that will be
processed (10 kilobytes = 10240 bytes.). | protected static final int | DEFAULT_BUFSIZEThe default length of the buffer used for processing a request. | protected static final byte[] | HEADER_SEPARATORA byte sequence that marks the end of header-part
(CRLFCRLF ). | protected static final byte[] | FIELD_SEPARATORA byte sequence that that follows a delimiter that will be
followed by an encapsulation (CRLF ). | protected static final byte[] | STREAM_TERMINATORA byte sequence that that follows a delimiter of the last
encapsulation in the stream (-- ). | private InputStream | inputThe input stream from which data is read. | private int | boundaryLengthThe length of the boundary token plus the leading CRLF-- . | private int | keepRegionThe amount of data, in bytes, that must be kept in the buffer in order
to detect delimiters reliably. | private byte[] | boundaryThe byte sequence that partitions the stream. | private int | bufSizeThe length of the buffer used for processing the request. | private byte[] | bufferThe buffer used for processing the request. | private int | headThe index of first valid character in the buffer.
0 <= head < bufSize | private int | tailThe index of last valid characer in the buffer + 1.
0 <= tail <= bufSize | private String | headerEncodingThe content encoding to use when reading headers. |
Constructors Summary |
---|
public MultipartStream()Default constructor.
// ----------------------------------------------------------- Constructors
| public MultipartStream(InputStream input, byte[] boundary, int bufSize) Constructs a MultipartStream with a custom size buffer.
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.
this.input = input;
this.bufSize = bufSize;
this.buffer = new byte[bufSize];
// We prepend CR/LF to the boundary to chop trailng CR/LF from
// body-data tokens.
this.boundary = new byte[boundary.length + 4];
this.boundaryLength = boundary.length + 4;
this.keepRegion = boundary.length + 3;
this.boundary[0] = 0x0D;
this.boundary[1] = 0x0A;
this.boundary[2] = 0x2D;
this.boundary[3] = 0x2D;
System.arraycopy(boundary, 0, this.boundary, 4, boundary.length);
head = 0;
tail = 0;
| public MultipartStream(InputStream input, byte[] boundary) Constructs a MultipartStream with a default size buffer.
this(input, boundary, DEFAULT_BUFSIZE);
|
Methods Summary |
---|
public static boolean | arrayequals(byte[] a, byte[] b, int count)Compares count first bytes in the arrays
a and b .
for (int i = 0; i < count; i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
| public int | discardBodyData() Reads body-data from the current
encapsulation and discards it.
Use this method to skip encapsulations you don't need or don't
understand.
boolean done = false;
int pad;
int pos;
int bytesRead;
int total = 0;
while (!done)
{
// Is boundary token present somewere in the buffer?
pos = findSeparator();
if (pos != -1)
{
// Write the rest of the data before the boundary.
total += pos - head;
head = pos;
done = true;
}
else
{
// Determine how much data should be kept in the
// buffer.
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
total += tail - head - pad;
// Move the data to the beging of the buffer.
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
// Refill buffer with new data.
head = 0;
bytesRead = input.read(buffer, pad, bufSize - pad);
// [pprrrrrrr]
if (bytesRead != -1)
{
tail = pad + bytesRead;
}
else
{
// The last pad amount is left in the buffer.
// Boundary can't be in there so signal an error
// condition.
total += pad;
throw new MalformedStreamException(
"Stream ended unexpectedly");
}
}
}
return total;
| protected int | findByte(byte value, int pos)Searches for a byte of specified value in the buffer ,
starting at the specified position .
for (int i = pos; i < tail; i++)
{
if (buffer[i] == value)
{
return i;
}
}
return -1;
| protected int | findSeparator()Searches for the boundary in the buffer
region delimited by head and tail .
int first;
int match = 0;
int maxpos = tail - boundaryLength;
for (first = head;
(first <= maxpos) && (match != boundaryLength);
first++)
{
first = findByte(boundary[0], first);
if (first == -1 || (first > maxpos))
{
return -1;
}
for (match = 1; match < boundaryLength; match++)
{
if (buffer[first + match] != boundary[match])
{
break;
}
}
}
if (match == boundaryLength)
{
return first - 1;
}
return -1;
| public java.lang.String | getHeaderEncoding()Retrieves the character encoding used when reading the headers of an
individual part. When not specified, or null , the platform
default encoding is used.
return headerEncoding;
| public int | readBodyData(java.io.OutputStream output)Reads body-data from the current
encapsulation and writes its contents into the
output Stream .
Arbitrary large amounts of data can be processed by this
method using a constant size buffer. (see {@link
#MultipartStream(InputStream,byte[],int) constructor}).
boolean done = false;
int pad;
int pos;
int bytesRead;
int total = 0;
while (!done)
{
// Is boundary token present somewere in the buffer?
pos = findSeparator();
if (pos != -1)
{
// Write the rest of the data before the boundary.
output.write(buffer, head, pos - head);
total += pos - head;
head = pos;
done = true;
}
else
{
// Determine how much data should be kept in the
// buffer.
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
// Write out the data belonging to the body-data.
output.write(buffer, head, tail - head - pad);
// Move the data to the beging of the buffer.
total += tail - head - pad;
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
// Refill buffer with new data.
head = 0;
bytesRead = input.read(buffer, pad, bufSize - pad);
// [pprrrrrrr]
if (bytesRead != -1)
{
tail = pad + bytesRead;
}
else
{
// The last pad amount is left in the buffer.
// Boundary can't be in there so write out the
// data you have and signal an error condition.
output.write(buffer, 0, pad);
output.flush();
total += pad;
throw new MalformedStreamException(
"Stream ended unexpectedly");
}
}
}
output.flush();
return total;
| public boolean | readBoundary()Skips a boundary token, and checks whether more
encapsulations are contained in the stream.
byte[] marker = new byte[2];
boolean nextChunk = false;
head += boundaryLength;
try
{
marker[0] = readByte();
marker[1] = readByte();
if (arrayequals(marker, STREAM_TERMINATOR, 2))
{
nextChunk = false;
}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
throw new MalformedStreamException(
"Unexpected characters follow a boundary");
}
}
catch (IOException e)
{
throw new MalformedStreamException("Stream ended unexpectedly");
}
return nextChunk;
| public byte | readByte()Reads a byte from the buffer , and refills it as
necessary.
// Buffer depleted ?
if (head == tail)
{
head = 0;
// Refill.
tail = input.read(buffer, head, bufSize);
if (tail == -1)
{
// No more data available.
throw new IOException("No more data is available");
}
}
return buffer[head++];
| public java.lang.String | readHeaders()Reads the header-part of the current
encapsulation .
Headers are returned verbatim to the input stream, including the
trailing CRLF marker. Parsing is left to the
application.
TODO allow limiting maximum header size to
protect against abuse.
int i = 0;
byte b[] = new byte[1];
// to support multi-byte characters
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int sizeMax = HEADER_PART_SIZE_MAX;
int size = 0;
while (i < 4)
{
try
{
b[0] = readByte();
}
catch (IOException e)
{
throw new MalformedStreamException("Stream ended unexpectedly");
}
size++;
if (b[0] == HEADER_SEPARATOR[i])
{
i++;
}
else
{
i = 0;
}
if (size <= sizeMax)
{
baos.write(b[0]);
}
}
String headers = null;
if (headerEncoding != null)
{
try
{
headers = baos.toString(headerEncoding);
}
catch (UnsupportedEncodingException e)
{
// Fall back to platform default if specified encoding is not
// supported.
headers = baos.toString();
}
}
else
{
headers = baos.toString();
}
return headers;
| public void | setBoundary(byte[] boundary)Changes the boundary token used for partitioning the stream.
This method allows single pass processing of nested multipart
streams.
The boundary token of the nested stream is required
to be of the same length as the boundary token in parent stream.
Restoring the parent stream boundary token after processing of a
nested stream is left to the application.
if (boundary.length != boundaryLength - 4)
{
throw new IllegalBoundaryException(
"The length of a boundary token can not be changed");
}
System.arraycopy(boundary, 0, this.boundary, 4, boundary.length);
| public void | setHeaderEncoding(java.lang.String encoding)Specifies the character encoding to be used when reading the headers of
individual parts. When not specified, or null , the platform
default encoding is used.
headerEncoding = encoding;
| public boolean | skipPreamble()Finds the beginning of the first encapsulation .
// First delimiter may be not preceeded with a CRLF.
System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2);
boundaryLength = boundary.length - 2;
try
{
// Discard all data up to the delimiter.
discardBodyData();
// Read boundary - if succeded, the stream contains an
// encapsulation.
return readBoundary();
}
catch (MalformedStreamException e)
{
return false;
}
finally
{
// Restore delimiter.
System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2);
boundaryLength = boundary.length;
boundary[0] = 0x0D;
boundary[1] = 0x0A;
}
| public java.lang.String | toString()Returns a string representation of this object.
StringBuffer sbTemp = new StringBuffer();
sbTemp.append("boundary='");
sbTemp.append(String.valueOf(boundary));
sbTemp.append("'\nbufSize=");
sbTemp.append(bufSize);
return sbTemp.toString();
|
|