1 /*
2  * ====================================================================
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  * ====================================================================
20  *
21  * This software consists of voluntary contributions made by many
22  * individuals on behalf of the Apache Software Foundation.  For more
23  * information on the Apache Software Foundation, please see
24  * <http://www.apache.org/>.
25  *
26  */
27 
28 package ch.boye.httpclientandroidlib.impl.io;
29 
30 import java.io.IOException;
31 import java.io.OutputStream;
32 import java.nio.ByteBuffer;
33 import java.nio.CharBuffer;
34 import java.nio.charset.CharsetEncoder;
35 import java.nio.charset.CoderResult;
36 
37 import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
38 import ch.boye.httpclientandroidlib.io.BufferInfo;
39 import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
40 import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
41 import ch.boye.httpclientandroidlib.protocol.HTTP;
42 import ch.boye.httpclientandroidlib.util.Args;
43 import ch.boye.httpclientandroidlib.util.Asserts;
44 import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
45 import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
46 
47 /**
48  * Abstract base class for session output buffers that stream data to
49  * an arbitrary {@link OutputStream}. This class buffers small chunks of
50  * output data in an internal byte array for optimal output performance.
51  * </p>
52  * {@link #writeLine(CharArrayBuffer)} and {@link #writeLine(String)} methods
53  * of this class use CR-LF as a line delimiter.
54  *
55  * @since 4.3
56  */
57 @NotThreadSafe
58 public class SessionOutputBufferImpl implements SessionOutputBuffer, BufferInfo {
59 
60     private static final byte[] CRLF = new byte[] {HTTP.CR, HTTP.LF};
61 
62     private final HttpTransportMetricsImpl metrics;
63     private final ByteArrayBuffer buffer;
64     private final int fragementSizeHint;
65     private final CharsetEncoder encoder;
66 
67     private OutputStream outstream;
68     private ByteBuffer bbuf;
69 
70     /**
71      * Creates new instance of SessionOutputBufferImpl.
72      *
73      * @param metrics HTTP transport metrics.
74      * @param buffersize buffer size. Must be a positive number.
75      * @param fragementSizeHint fragment size hint defining a minimal size of a fragment
76      *   that should be written out directly to the socket bypassing the session buffer.
77      *   Value <code>0</code> disables fragment buffering.
78      * @param charencoder charencoder to be used for encoding HTTP protocol elements.
79      *   If <code>null</code> simple type cast will be used for char to byte conversion.
80      */
SessionOutputBufferImpl( final HttpTransportMetricsImpl metrics, final int buffersize, final int fragementSizeHint, final CharsetEncoder charencoder)81     public SessionOutputBufferImpl(
82             final HttpTransportMetricsImpl metrics,
83             final int buffersize,
84             final int fragementSizeHint,
85             final CharsetEncoder charencoder) {
86         super();
87         Args.positive(buffersize, "Buffer size");
88         Args.notNull(metrics, "HTTP transport metrcis");
89         this.metrics = metrics;
90         this.buffer = new ByteArrayBuffer(buffersize);
91         this.fragementSizeHint = fragementSizeHint >= 0 ? fragementSizeHint : 0;
92         this.encoder = charencoder;
93     }
94 
SessionOutputBufferImpl( final HttpTransportMetricsImpl metrics, final int buffersize)95     public SessionOutputBufferImpl(
96             final HttpTransportMetricsImpl metrics,
97             final int buffersize) {
98         this(metrics, buffersize, buffersize, null);
99     }
100 
bind(final OutputStream outstream)101     public void bind(final OutputStream outstream) {
102         this.outstream = outstream;
103     }
104 
isBound()105     public boolean isBound() {
106         return this.outstream != null;
107     }
108 
capacity()109     public int capacity() {
110         return this.buffer.capacity();
111     }
112 
length()113     public int length() {
114         return this.buffer.length();
115     }
116 
available()117     public int available() {
118         return capacity() - length();
119     }
120 
streamWrite(final byte[] b, final int off, final int len)121     private void streamWrite(final byte[] b, final int off, final int len) throws IOException {
122         Asserts.notNull(outstream, "Output stream");
123         this.outstream.write(b, off, len);
124     }
125 
flushStream()126     private void flushStream() throws IOException {
127         if (this.outstream != null) {
128             this.outstream.flush();
129         }
130     }
131 
flushBuffer()132     private void flushBuffer() throws IOException {
133         final int len = this.buffer.length();
134         if (len > 0) {
135             streamWrite(this.buffer.buffer(), 0, len);
136             this.buffer.clear();
137             this.metrics.incrementBytesTransferred(len);
138         }
139     }
140 
flush()141     public void flush() throws IOException {
142         flushBuffer();
143         flushStream();
144     }
145 
write(final byte[] b, final int off, final int len)146     public void write(final byte[] b, final int off, final int len) throws IOException {
147         if (b == null) {
148             return;
149         }
150         // Do not want to buffer large-ish chunks
151         // if the byte array is larger then MIN_CHUNK_LIMIT
152         // write it directly to the output stream
153         if (len > this.fragementSizeHint || len > this.buffer.capacity()) {
154             // flush the buffer
155             flushBuffer();
156             // write directly to the out stream
157             streamWrite(b, off, len);
158             this.metrics.incrementBytesTransferred(len);
159         } else {
160             // Do not let the buffer grow unnecessarily
161             final int freecapacity = this.buffer.capacity() - this.buffer.length();
162             if (len > freecapacity) {
163                 // flush the buffer
164                 flushBuffer();
165             }
166             // buffer
167             this.buffer.append(b, off, len);
168         }
169     }
170 
write(final byte[] b)171     public void write(final byte[] b) throws IOException {
172         if (b == null) {
173             return;
174         }
175         write(b, 0, b.length);
176     }
177 
write(final int b)178     public void write(final int b) throws IOException {
179         if (this.fragementSizeHint > 0) {
180             if (this.buffer.isFull()) {
181                 flushBuffer();
182             }
183             this.buffer.append(b);
184         } else {
185             flushBuffer();
186             this.outstream.write(b);
187         }
188     }
189 
190     /**
191      * Writes characters from the specified string followed by a line delimiter
192      * to this session buffer.
193      * <p>
194      * This method uses CR-LF as a line delimiter.
195      *
196      * @param      s   the line.
197      * @exception  IOException  if an I/O error occurs.
198      */
writeLine(final String s)199     public void writeLine(final String s) throws IOException {
200         if (s == null) {
201             return;
202         }
203         if (s.length() > 0) {
204             if (this.encoder == null) {
205                 for (int i = 0; i < s.length(); i++) {
206                     write(s.charAt(i));
207                 }
208             } else {
209                 final CharBuffer cbuf = CharBuffer.wrap(s);
210                 writeEncoded(cbuf);
211             }
212         }
213         write(CRLF);
214     }
215 
216     /**
217      * Writes characters from the specified char array followed by a line
218      * delimiter to this session buffer.
219      * <p>
220      * This method uses CR-LF as a line delimiter.
221      *
222      * @param      charbuffer the buffer containing chars of the line.
223      * @exception  IOException  if an I/O error occurs.
224      */
writeLine(final CharArrayBuffer charbuffer)225     public void writeLine(final CharArrayBuffer charbuffer) throws IOException {
226         if (charbuffer == null) {
227             return;
228         }
229         if (this.encoder == null) {
230             int off = 0;
231             int remaining = charbuffer.length();
232             while (remaining > 0) {
233                 int chunk = this.buffer.capacity() - this.buffer.length();
234                 chunk = Math.min(chunk, remaining);
235                 if (chunk > 0) {
236                     this.buffer.append(charbuffer, off, chunk);
237                 }
238                 if (this.buffer.isFull()) {
239                     flushBuffer();
240                 }
241                 off += chunk;
242                 remaining -= chunk;
243             }
244         } else {
245             final CharBuffer cbuf = CharBuffer.wrap(charbuffer.buffer(), 0, charbuffer.length());
246             writeEncoded(cbuf);
247         }
248         write(CRLF);
249     }
250 
writeEncoded(final CharBuffer cbuf)251     private void writeEncoded(final CharBuffer cbuf) throws IOException {
252         if (!cbuf.hasRemaining()) {
253             return;
254         }
255         if (this.bbuf == null) {
256             this.bbuf = ByteBuffer.allocate(1024);
257         }
258         this.encoder.reset();
259         while (cbuf.hasRemaining()) {
260             final CoderResult result = this.encoder.encode(cbuf, this.bbuf, true);
261             handleEncodingResult(result);
262         }
263         final CoderResult result = this.encoder.flush(this.bbuf);
264         handleEncodingResult(result);
265         this.bbuf.clear();
266     }
267 
handleEncodingResult(final CoderResult result)268     private void handleEncodingResult(final CoderResult result) throws IOException {
269         if (result.isError()) {
270             result.throwException();
271         }
272         this.bbuf.flip();
273         while (this.bbuf.hasRemaining()) {
274             write(this.bbuf.get());
275         }
276         this.bbuf.compact();
277     }
278 
getMetrics()279     public HttpTransportMetrics getMetrics() {
280         return this.metrics;
281     }
282 
283 }
284