001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.io;
029
030import java.io.IOException;
031import java.io.OutputStream;
032
033import org.apache.http.io.SessionOutputBuffer;
034
035/**
036 * Implements chunked transfer coding. The content is sent in small chunks.
037 * Entities transferred using this output stream can be of unlimited length.
038 * Writes are buffered to an internal buffer (2048 default size).
039 * <p>
040 * Note that this class NEVER closes the underlying stream, even when close
041 * gets called.  Instead, the stream will be marked as closed and no further
042 * output will be permitted.
043 *
044 *
045 * @since 4.0
046 */
047public class ChunkedOutputStream extends OutputStream {
048
049    // ----------------------------------------------------- Instance Variables
050    private final SessionOutputBuffer out;
051
052    private final byte[] cache;
053
054    private int cachePosition = 0;
055
056    private boolean wroteLastChunk = false;
057
058    /** True if the stream is closed. */
059    private boolean closed = false;
060
061    /**
062     * Wraps a session output buffer and chunk-encodes the output.
063     *
064     * @param out The session output buffer
065     * @param bufferSize The minimum chunk size (excluding last chunk)
066     * @throws IOException not thrown
067     *
068     * @deprecated (4.3) use {@link ChunkedOutputStream#ChunkedOutputStream(int, SessionOutputBuffer)}
069     */
070    @Deprecated
071    public ChunkedOutputStream(final SessionOutputBuffer out, final int bufferSize)
072            throws IOException {
073        this(bufferSize, out);
074    }
075
076    /**
077     * Wraps a session output buffer and chunks the output. The default buffer
078     * size of 2048 was chosen because the chunk overhead is less than 0.5%
079     *
080     * @param out       the output buffer to wrap
081     * @throws IOException not thrown
082     *
083     * @deprecated (4.3) use {@link ChunkedOutputStream#ChunkedOutputStream(int, SessionOutputBuffer)}
084     */
085    @Deprecated
086    public ChunkedOutputStream(final SessionOutputBuffer out)
087            throws IOException {
088        this(2048, out);
089    }
090
091    /**
092     * Wraps a session output buffer and chunk-encodes the output.
093     *
094     * @param bufferSize The minimum chunk size (excluding last chunk)
095     * @param out The session output buffer
096     */
097    public ChunkedOutputStream(final int bufferSize, final SessionOutputBuffer out) {
098        super();
099        this.cache = new byte[bufferSize];
100        this.out = out;
101    }
102
103    /**
104     * Writes the cache out onto the underlying stream
105     */
106    protected void flushCache() throws IOException {
107        if (this.cachePosition > 0) {
108            this.out.writeLine(Integer.toHexString(this.cachePosition));
109            this.out.write(this.cache, 0, this.cachePosition);
110            this.out.writeLine("");
111            this.cachePosition = 0;
112        }
113    }
114
115    /**
116     * Writes the cache and bufferToAppend to the underlying stream
117     * as one large chunk
118     */
119    protected void flushCacheWithAppend(final byte bufferToAppend[], final int off, final int len) throws IOException {
120        this.out.writeLine(Integer.toHexString(this.cachePosition + len));
121        this.out.write(this.cache, 0, this.cachePosition);
122        this.out.write(bufferToAppend, off, len);
123        this.out.writeLine("");
124        this.cachePosition = 0;
125    }
126
127    protected void writeClosingChunk() throws IOException {
128        // Write the final chunk.
129        this.out.writeLine("0");
130        this.out.writeLine("");
131    }
132
133    // ----------------------------------------------------------- Public Methods
134    /**
135     * Must be called to ensure the internal cache is flushed and the closing
136     * chunk is written.
137     * @throws IOException in case of an I/O error
138     */
139    public void finish() throws IOException {
140        if (!this.wroteLastChunk) {
141            flushCache();
142            writeClosingChunk();
143            this.wroteLastChunk = true;
144        }
145    }
146
147    // -------------------------------------------- OutputStream Methods
148    @Override
149    public void write(final int b) throws IOException {
150        if (this.closed) {
151            throw new IOException("Attempted write to closed stream.");
152        }
153        this.cache[this.cachePosition] = (byte) b;
154        this.cachePosition++;
155        if (this.cachePosition == this.cache.length) {
156            flushCache();
157        }
158    }
159
160    /**
161     * Writes the array. If the array does not fit within the buffer, it is
162     * not split, but rather written out as one large chunk.
163     */
164    @Override
165    public void write(final byte b[]) throws IOException {
166        write(b, 0, b.length);
167    }
168
169    /**
170     * Writes the array. If the array does not fit within the buffer, it is
171     * not split, but rather written out as one large chunk.
172     */
173    @Override
174    public void write(final byte src[], final int off, final int len) throws IOException {
175        if (this.closed) {
176            throw new IOException("Attempted write to closed stream.");
177        }
178        if (len >= this.cache.length - this.cachePosition) {
179            flushCacheWithAppend(src, off, len);
180        } else {
181            System.arraycopy(src, off, cache, this.cachePosition, len);
182            this.cachePosition += len;
183        }
184    }
185
186    /**
187     * Flushes the content buffer and the underlying stream.
188     */
189    @Override
190    public void flush() throws IOException {
191        flushCache();
192        this.out.flush();
193    }
194
195    /**
196     * Finishes writing to the underlying stream, but does NOT close the underlying stream.
197     */
198    @Override
199    public void close() throws IOException {
200        if (!this.closed) {
201            this.closed = true;
202            finish();
203            this.out.flush();
204        }
205    }
206}