Java tutorial
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); } }