An InputStream that implements HTTP/1.1 chunking
package net.matuschek.util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Vector;
// ChunkedInputStream - an InputStream that implements HTTP/1.1 chunking
//
// Copyright (C) 1996,1998 by Jef Poskanzer <jef@acme.com>. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/
/**
* Modifications done by Daniel Matuschek (daniel@matuschek.net)
* - modified JavaDoc documentation
* - adapted to Java 1.2, removed deprecated DataInputStream.readLine() method
* - replaced DataInputStream by InputStream (there was no need for a
* DatainputStream, not idea why this was used in the original version)
* - fixed a bug (there is an CRLF after every the data block)
*/
/**
* An InputStream that implements HTTP/1.1 chunking.
* <P>
* This class lets a Servlet read its request data as an HTTP/1.1 chunked
* stream. Chunked streams are a way to send arbitrary-length data without
* having to know beforehand how much you're going to send. They are
* introduced by a "Transfer-Encoding: chunked" header, so if such a header
* appears in an HTTP request you should use this class to read any data.
* <P>
* Sample usage:
* <BLOCKQUOTE><PRE><CODE>
* InputStream in = req.getInputStream();
* if ( "chunked".equals( req.getHeader( "Transfer-Encoding" ) ) )
* in = new ChunkedInputStream( in );
* </CODE></PRE></BLOCKQUOTE>
* <P>
* Because it would be impolite to make the authors of every Servlet include
* the above code, this is general done at the server level so that it
* happens automatically. Servlet authors will generally not create
* ChunkedInputStreams. This is in contrast with ChunkedOutputStream,
* which Servlets have to call themselves if they want to use it.
* <P>
* <A HREF="/resources/classes/Acme/Serve/servlet/http/ChunkedInputStream.java">Fetch the software.</A><BR>
* <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
*
* @author Jef Poskanzer
* @author Daniel Matuschek
* @version $Id: ChunkedInputStream.java,v 1.6 2002/05/31 14:45:56 matuschd Exp $
*/
public class ChunkedInputStream extends FilterInputStream
{
private int contentLength;
private byte[] b1 = new byte[1];
/** number of bytes available in the current chunk */
private int chunkCount = 0;
private Vector<String> footerNames = null;
private Vector<String> footerValues = null;
/**
* Make a ChunkedInputStream.
*/
public ChunkedInputStream( InputStream in )
{
super(in);
contentLength = 0;
}
/**
* The FilterInputStream implementation of the single-byte read()
* method just reads directly from the underlying stream. We want
* to go through our own read-block method, so we have to override.
* Seems like FilterInputStream really ought to do this itself.
*/
public int read() throws IOException
{
if (read(b1,0,1) == -1 ) {
return -1;
}
return b1[0];
}
/**
* Reads into an array of bytes.
* @param b the buffer into which the data is read
* @param off the start offset of the data
* @param len the maximum number of bytes read
* @return the actual number of bytes read, or -1 on EOF
* @exception IOException if an I/O error has occurred
*/
public int read( byte[] b, int off, int len ) throws IOException
{
if (chunkCount == 0) {
startChunk();
if (chunkCount == 0) {
return -1;
}
}
int toRead = Math.min( chunkCount, len );
int r = in.read( b, off, toRead );
if ( r != -1 ) {
chunkCount -= r;
}
return r;
}
/**
* Reads the start of a chunk.
*/
private void startChunk() throws IOException
{
String line = readLine();
if (line.equals("")) {
line=readLine();
}
try {
chunkCount = Integer.parseInt(line.trim(),16);
} catch (NumberFormatException e) {
throw new IOException("malformed chunk ("+line+")");
}
contentLength += chunkCount;
if ( chunkCount == 0 ) {
readFooters();
}
}
/**
* Reads any footers.
*/
private void readFooters() throws IOException
{
footerNames = new Vector<String>();
footerValues = new Vector<String>();
String line;
while ( true ) {
line = readLine();
if ( line.length() == 0 )
break;
int colon = line.indexOf( ':' );
if ( colon != -1 )
{
String name = line.substring( 0, colon ).toLowerCase();
String value = line.substring( colon + 1 ).trim();
footerNames.addElement( name.toLowerCase() );
footerValues.addElement( value );
}
}
}
/**
* Returns the value of a footer field, or null if not known.
* Footers come at the end of a chunked stream, so trying to
* retrieve them before the stream has given an EOF will return
* only nulls.
* @param name the footer field name
*/
public String getFooter( String name )
{
if ( ! isDone() )
return null;
int i = footerNames.indexOf( name.toLowerCase() );
if ( i == -1 )
return null;
return (String) footerValues.elementAt( i );
}
/**
* Returns an Enumeration of the footer names.
*/
public Enumeration getFooters()
{
if ( ! isDone() )
return null;
return footerNames.elements();
}
/**
* Returns the size of the request entity data, or -1 if not known.
*/
public int getContentLength()
{
if (! isDone()) {
return -1;
}
return contentLength;
}
/**
* Tells whether the stream has gotten to its end yet. Remembering
* whether you've gotten an EOF works fine too, but this is a convenient
* predicate. java.io.InputStream should probably have its own isEof()
* predicate.
*/
public boolean isDone()
{
return footerNames != null;
}
/**
* ChunkedInputStream used DataInputStream.readLine() before. This method
* is deprecated, therefore we will it replace by our own method.
* Because the chunk lines only use 7bit ASCII, we can use the
* system default encoding
* The data lines itself will not be read using this readLine method
* but by a block read
*/
protected String readLine()
throws IOException
{
final byte CR=13;
final byte LF=10;
ByteBuffer buff = new ByteBuffer();
byte b=0;
int i=0;
do {
b = (byte)this.in.read();
if (b != LF) {
buff.append(b);
}
i++;
} while ((b != LF));
// according to the RFC there must be a CR before the LF, but some
// web servers don't do this :-(
byte[] byteBuff = buff.getContent();
if (byteBuff.length == 0) {
return "";
}
if (byteBuff[byteBuff.length-1] != CR) {
return new String(byteBuff);
} else {
return new String(byteBuff,0,byteBuff.length-1);
}
}
}
/*********************************************
Copyright (c) 2001 by Daniel Matuschek
*********************************************/
/**
* A ByteBuffer implements a growable byte array. You can simple
* add bytes like you do it using a Vector, but internally the buffer
* is implemented as a real array of bytes. This increases memory usage.
*
* @author Daniel Matuschek
* @version $Id $
*/
class ByteBuffer {
protected final int INITIALSIZE=1024;
protected int used = 0;
protected int size = 0;
protected byte[] buff =null;
/**
* Initializes a new ByteBuffer object and creates
* a temporary buffer array of a predefined initial size.
* If you want to set your own initial size, use the <code>setSize</code>
* method after initializing the object.
*
*/
public ByteBuffer() {
size=INITIALSIZE;
buff=new byte[INITIALSIZE];
}
/**
* Appends a byte to the end of the buffer
*
* If the currently reserved memory is used, the size of the
* internal buffer will be doubled.
* In this case the memory usage will temprary increase by factor 3
* because it need a temporary storage for the old data.
*
* Be sure that you have enough heap memory !
*
* @param b byte to append
*/
public void append(byte b) {
if (used >= size) {
doubleBuffer();
}
buff[used]=b;
used++;
}
/**
* @return the number of bytes stored in the buffer
*/
public int length() {
return used;
}
/**
* @return the buffer contents as a byte array
*/
public byte[] getContent() {
byte[] b = new byte[used];
for (int i=0; i<used; i++) {
b[i]=buff[i];
}
return b;
}
/**
* removes all contents in the buffer
*/
public void clean() {
used=0;
}
/**
* Sets the size of the internal buffer to
* the given value. This is useful, if the size of the
* data that should be stored is known.
* @param size size of the buffer in Bytes
*/
public void setSize(int size) {
// if we have already used more data, ignore it !
if (size < used) {
return;
}
this.size=size;
// create a new (larger) array
byte[] newBuff = new byte[size];
// copy contents
for (int i=0; i<used; i++) {
newBuff[i]=buff[i];
}
buff=newBuff;
}
/**
* Print the buffer content as a String (use it for debugging only !)
* @return a String containing every byte in the buffer as a character
*/
public String toString() {
StringBuffer sb = new StringBuffer(buff.length);
for (int i=0; i<used; i++) {
sb.append(buff[i]);
}
return sb.toString();
}
/**
* doubles the size of the internal buffer
*/
protected void doubleBuffer() {
// increase size
setSize(size*2);
}
}
Related examples in the same category