1/*
2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/impl/io/ContentLengthInputStream.java $
3 * $Revision: 652091 $
4 * $Date: 2008-04-29 13:41:07 -0700 (Tue, 29 Apr 2008) $
5 *
6 * ====================================================================
7 * Licensed to the Apache Software Foundation (ASF) under one
8 * or more contributor license agreements.  See the NOTICE file
9 * distributed with this work for additional information
10 * regarding copyright ownership.  The ASF licenses this file
11 * to you under the Apache License, Version 2.0 (the
12 * "License"); you may not use this file except in compliance
13 * with the License.  You may obtain a copy of the License at
14 *
15 *   http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing,
18 * software distributed under the License is distributed on an
19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20 * KIND, either express or implied.  See the License for the
21 * specific language governing permissions and limitations
22 * under the License.
23 * ====================================================================
24 *
25 * This software consists of voluntary contributions made by many
26 * individuals on behalf of the Apache Software Foundation.  For more
27 * information on the Apache Software Foundation, please see
28 * <http://www.apache.org/>.
29 *
30 */
31
32package org.apache.http.impl.io;
33
34import java.io.IOException;
35import java.io.InputStream;
36
37import org.apache.http.io.SessionInputBuffer;
38
39/**
40 * Stream that cuts off after a specified number of bytes.
41 * Note that this class NEVER closes the underlying stream, even when close
42 * gets called.  Instead, it will read until the "end" of its chunking on
43 * close, which allows for the seamless execution of subsequent HTTP 1.1
44 * requests, while not requiring the client to remember to read the entire
45 * contents of the response.
46 *
47 * <p>Implementation note: Choices abound. One approach would pass
48 * through the {@link InputStream#mark} and {@link InputStream#reset} calls to
49 * the underlying stream.  That's tricky, though, because you then have to
50 * start duplicating the work of keeping track of how much a reset rewinds.
51 * Further, you have to watch out for the "readLimit", and since the semantics
52 * for the readLimit leave room for differing implementations, you might get
53 * into a lot of trouble.</p>
54 *
55 * <p>Alternatively, you could make this class extend
56 * {@link java.io.BufferedInputStream}
57 * and then use the protected members of that class to avoid duplicated effort.
58 * That solution has the side effect of adding yet another possible layer of
59 * buffering.</p>
60 *
61 * <p>Then, there is the simple choice, which this takes - simply don't
62 * support {@link InputStream#mark} and {@link InputStream#reset}.  That choice
63 * has the added benefit of keeping this class very simple.</p>
64 *
65 * @author Ortwin Glueck
66 * @author Eric Johnson
67 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
68 *
69 * @since 4.0
70 */
71public class ContentLengthInputStream extends InputStream {
72
73    private static final int BUFFER_SIZE = 2048;
74    /**
75     * The maximum number of bytes that can be read from the stream. Subsequent
76     * read operations will return -1.
77     */
78    private long contentLength;
79
80    /** The current position */
81    private long pos = 0;
82
83    /** True if the stream is closed. */
84    private boolean closed = false;
85
86    /**
87     * Wrapped input stream that all calls are delegated to.
88     */
89    private SessionInputBuffer in = null;
90
91    /**
92     * Creates a new length limited stream
93     *
94     * @param in The session input buffer to wrap
95     * @param contentLength The maximum number of bytes that can be read from
96     * the stream. Subsequent read operations will return -1.
97     */
98    public ContentLengthInputStream(final SessionInputBuffer in, long contentLength) {
99        super();
100        if (in == null) {
101            throw new IllegalArgumentException("Input stream may not be null");
102        }
103        if (contentLength < 0) {
104            throw new IllegalArgumentException("Content length may not be negative");
105        }
106        this.in = in;
107        this.contentLength = contentLength;
108    }
109
110    /**
111     * <p>Reads until the end of the known length of content.</p>
112     *
113     * <p>Does not close the underlying socket input, but instead leaves it
114     * primed to parse the next response.</p>
115     * @throws IOException If an IO problem occurs.
116     */
117    public void close() throws IOException {
118        if (!closed) {
119            try {
120                byte buffer[] = new byte[BUFFER_SIZE];
121                while (read(buffer) >= 0) {
122                }
123            } finally {
124                // close after above so that we don't throw an exception trying
125                // to read after closed!
126                closed = true;
127            }
128        }
129    }
130
131
132    /**
133     * Read the next byte from the stream
134     * @return The next byte or -1 if the end of stream has been reached.
135     * @throws IOException If an IO problem occurs
136     * @see java.io.InputStream#read()
137     */
138    public int read() throws IOException {
139        if (closed) {
140            throw new IOException("Attempted read from closed stream.");
141        }
142
143        if (pos >= contentLength) {
144            return -1;
145        }
146        pos++;
147        return this.in.read();
148    }
149
150    /**
151     * Does standard {@link InputStream#read(byte[], int, int)} behavior, but
152     * also notifies the watcher when the contents have been consumed.
153     *
154     * @param b     The byte array to fill.
155     * @param off   Start filling at this position.
156     * @param len   The number of bytes to attempt to read.
157     * @return The number of bytes read, or -1 if the end of content has been
158     *  reached.
159     *
160     * @throws java.io.IOException Should an error occur on the wrapped stream.
161     */
162    public int read (byte[] b, int off, int len) throws java.io.IOException {
163        if (closed) {
164            throw new IOException("Attempted read from closed stream.");
165        }
166
167        if (pos >= contentLength) {
168            return -1;
169        }
170
171        if (pos + len > contentLength) {
172            len = (int) (contentLength - pos);
173        }
174        int count = this.in.read(b, off, len);
175        pos += count;
176        return count;
177    }
178
179
180    /**
181     * Read more bytes from the stream.
182     * @param b The byte array to put the new data in.
183     * @return The number of bytes read into the buffer.
184     * @throws IOException If an IO problem occurs
185     * @see java.io.InputStream#read(byte[])
186     */
187    public int read(byte[] b) throws IOException {
188        return read(b, 0, b.length);
189    }
190
191    /**
192     * Skips and discards a number of bytes from the input stream.
193     * @param n The number of bytes to skip.
194     * @return The actual number of bytes skipped. <= 0 if no bytes
195     * are skipped.
196     * @throws IOException If an error occurs while skipping bytes.
197     * @see InputStream#skip(long)
198     */
199    public long skip(long n) throws IOException {
200        if (n <= 0) {
201            return 0;
202        }
203        byte[] buffer = new byte[BUFFER_SIZE];
204        // make sure we don't skip more bytes than are
205        // still available
206        long remaining = Math.min(n, this.contentLength - this.pos);
207        // skip and keep track of the bytes actually skipped
208        long count = 0;
209        while (remaining > 0) {
210            int l = read(buffer, 0, (int)Math.min(BUFFER_SIZE, remaining));
211            if (l == -1) {
212                break;
213            }
214            count += l;
215            remaining -= l;
216        }
217        this.pos += count;
218        return count;
219    }
220}
221