/* * Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.net.www.http; import java.io.*; /** * OutputStream that sends the output to the underlying stream using chunked * encoding as specified in RFC 2068. */ public class ChunkedOutputStream extends PrintStream { /* Default chunk size (including chunk header) if not specified */ static final int DEFAULT_CHUNK_SIZE = 4096; private static final byte[] CRLF = {'\r', '\n'}; private static final int CRLF_SIZE = CRLF.length; private static final byte[] FOOTER = CRLF; private static final int FOOTER_SIZE = CRLF_SIZE; private static final byte[] EMPTY_CHUNK_HEADER = getHeader(0); private static final int EMPTY_CHUNK_HEADER_SIZE = getHeaderSize(0); /* internal buffer */ private byte buf[]; /* size of data (excluding footers and headers) already stored in buf */ private int size; /* current index in buf (i.e. buf[count] */ private int count; /* number of bytes to be filled up to complete a data chunk * currently being built */ private int spaceInCurrentChunk; /* underlying stream */ private PrintStream out; /* the chunk size we use */ private int preferredChunkDataSize; private int preferedHeaderSize; private int preferredChunkGrossSize; /* header for a complete Chunk */ private byte[] completeHeader; /* return the size of the header for a particular chunk size */ private static int getHeaderSize(int size) { return (Integer.toHexString(size)).length() + CRLF_SIZE; } /* return a header for a particular chunk size */ private static byte[] getHeader(int size){ try { String hexStr = Integer.toHexString(size); byte[] hexBytes = hexStr.getBytes("US-ASCII"); byte[] header = new byte[getHeaderSize(size)]; for (int i=0; i<hexBytes.length; i++) header[i] = hexBytes[i]; header[hexBytes.length] = CRLF[0]; header[hexBytes.length+1] = CRLF[1]; return header; } catch (java.io.UnsupportedEncodingException e) { /* This should never happen */ throw new InternalError(e.getMessage(), e); } } public ChunkedOutputStream(PrintStream o) { this(o, DEFAULT_CHUNK_SIZE); } public ChunkedOutputStream(PrintStream o, int size) { super(o); out = o; if (size <= 0) { size = DEFAULT_CHUNK_SIZE; } /* Adjust the size to cater for the chunk header - eg: if the * preferred chunk size is 1k this means the chunk size should * be 1017 bytes (differs by 7 from preferred size because of * 3 bytes for chunk size in hex and CRLF (header) and CRLF (footer)). * * If headerSize(adjusted_size) is shorter then headerSize(size) * then try to use the extra byte unless headerSize(adjusted_size+1) * increases back to headerSize(size) */ if (size > 0) { int adjusted_size = size - getHeaderSize(size) - FOOTER_SIZE; if (getHeaderSize(adjusted_size+1) < getHeaderSize(size)){ adjusted_size++; } size = adjusted_size; } if (size > 0) { preferredChunkDataSize = size; } else { preferredChunkDataSize = DEFAULT_CHUNK_SIZE - getHeaderSize(DEFAULT_CHUNK_SIZE) - FOOTER_SIZE; } preferedHeaderSize = getHeaderSize(preferredChunkDataSize); preferredChunkGrossSize = preferedHeaderSize + preferredChunkDataSize + FOOTER_SIZE; completeHeader = getHeader(preferredChunkDataSize); /* start with an initial buffer */ buf = new byte[preferredChunkGrossSize]; reset(); } /* * Flush a buffered, completed chunk to an underlying stream. If the data in * the buffer is insufficient to build up a chunk of "preferredChunkSize" * then the data do not get flushed unless flushAll is true. If flushAll is * true then the remaining data builds up a last chunk which size is smaller * than preferredChunkSize, and then the last chunk gets flushed to * underlying stream. If flushAll is true and there is no data in a buffer * at all then an empty chunk (containing a header only) gets flushed to * underlying stream. */ private void flush(boolean flushAll) { if (spaceInCurrentChunk == 0) { /* flush a completed chunk to underlying stream */ out.write(buf, 0, preferredChunkGrossSize); out.flush(); reset(); } else if (flushAll){ /* complete the last chunk and flush it to underlying stream */ if (size > 0){ /* adjust a header start index in case the header of the last * chunk is shorter then preferedHeaderSize */ int adjustedHeaderStartIndex = preferedHeaderSize - getHeaderSize(size); /* write header */ System.arraycopy(getHeader(size), 0, buf, adjustedHeaderStartIndex, getHeaderSize(size)); /* write footer */ buf[count++] = FOOTER[0]; buf[count++] = FOOTER[1]; //send the last chunk to underlying stream out.write(buf, adjustedHeaderStartIndex, count - adjustedHeaderStartIndex); } else { //send an empty chunk (containing just a header) to underlying stream out.write(EMPTY_CHUNK_HEADER, 0, EMPTY_CHUNK_HEADER_SIZE); } out.flush(); reset(); } } @Override public boolean checkError() { return out.checkError(); } /* Check that the output stream is still open */ private void ensureOpen() { if (out == null) setError(); } /* * Writes data from b[] to an internal buffer and stores the data as data * chunks of a following format: {Data length in Hex}{CRLF}{data}{CRLF} * The size of the data is preferredChunkSize. As soon as a completed chunk * is read from b[] a process of reading from b[] suspends, the chunk gets * flushed to the underlying stream and then the reading process from b[] * continues. When there is no more sufficient data in b[] to build up a * chunk of preferredChunkSize size the data get stored as an incomplete * chunk of a following format: {space for data length}{CRLF}{data} * The size of the data is of course smaller than preferredChunkSize. */ @Override public synchronized void write(byte b[], int off, int len) { ensureOpen(); if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } /* if b[] contains enough data then one loop cycle creates one complete * data chunk with a header, body and a footer, and then flushes the * chunk to the underlying stream. Otherwise, the last loop cycle * creates incomplete data chunk with empty header and with no footer * and stores this incomplete chunk in an internal buffer buf[] */ int bytesToWrite = len; int inputIndex = off; /* the index of the byte[] currently being written */ do { /* enough data to complete a chunk */ if (bytesToWrite >= spaceInCurrentChunk) { /* header */ for (int i=0; i<completeHeader.length; i++) buf[i] = completeHeader[i]; /* data */ System.arraycopy(b, inputIndex, buf, count, spaceInCurrentChunk); inputIndex += spaceInCurrentChunk; bytesToWrite -= spaceInCurrentChunk; count += spaceInCurrentChunk; /* footer */ buf[count++] = FOOTER[0]; buf[count++] = FOOTER[1]; spaceInCurrentChunk = 0; //chunk is complete flush(false); if (checkError()){ break; } } /* not enough data to build a chunk */ else { /* header */ /* do not write header if not enough bytes to build a chunk yet */ /* data */ System.arraycopy(b, inputIndex, buf, count, bytesToWrite); count += bytesToWrite; size += bytesToWrite; spaceInCurrentChunk -= bytesToWrite; bytesToWrite = 0; /* footer */ /* do not write header if not enough bytes to build a chunk yet */ } } while (bytesToWrite > 0); } @Override public synchronized void write(int _b) { byte b[] = {(byte)_b}; write(b, 0, 1); } public synchronized void reset() { count = preferedHeaderSize; size = 0; spaceInCurrentChunk = preferredChunkDataSize; } public int size() { return size; } @Override public synchronized void close() { ensureOpen(); /* if we have buffer a chunked send it */ if (size > 0) { flush(true); } /* send a zero length chunk */ flush(true); /* don't close the underlying stream */ out = null; } @Override public synchronized void flush() { ensureOpen(); if (size > 0) { flush(true); } } }