View Javadoc
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 org.apache.http.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 org.apache.http.io.BufferInfo;
38  import org.apache.http.io.HttpTransportMetrics;
39  import org.apache.http.io.SessionOutputBuffer;
40  import org.apache.http.protocol.HTTP;
41  import org.apache.http.util.Args;
42  import org.apache.http.util.Asserts;
43  import org.apache.http.util.ByteArrayBuffer;
44  import org.apache.http.util.CharArrayBuffer;
45  
46  /**
47   * Abstract base class for session output buffers that stream data to
48   * an arbitrary {@link OutputStream}. This class buffers small chunks of
49   * output data in an internal byte array for optimal output performance.
50   * <p>
51   * {@link #writeLine(CharArrayBuffer)} and {@link #writeLine(String)} methods
52   * of this class use CR-LF as a line delimiter.
53   *
54   * @since 4.3
55   */
56  public class SessionOutputBufferImpl implements SessionOutputBuffer, BufferInfo {
57  
58      private static final byte[] CRLF = new byte[] {HTTP.CR, HTTP.LF};
59  
60      private final HttpTransportMetricsImpl metrics;
61      private final ByteArrayBuffer buffer;
62      private final int fragementSizeHint;
63      private final CharsetEncoder encoder;
64  
65      private OutputStream outStream;
66      private ByteBuffer bbuf;
67  
68      /**
69       * Creates new instance of SessionOutputBufferImpl.
70       *
71       * @param metrics HTTP transport metrics.
72       * @param bufferSize buffer size. Must be a positive number.
73       * @param fragementSizeHint fragment size hint defining a minimal size of a fragment
74       *   that should be written out directly to the socket bypassing the session buffer.
75       *   Value {@code 0} disables fragment buffering.
76       * @param charEncoder charEncoder to be used for encoding HTTP protocol elements.
77       *   If {@code null} simple type cast will be used for char to byte conversion.
78       */
79      public SessionOutputBufferImpl(
80              final HttpTransportMetricsImpl metrics,
81              final int bufferSize,
82              final int fragementSizeHint,
83              final CharsetEncoder charEncoder) {
84          super();
85          Args.positive(bufferSize, "Buffer size");
86          Args.notNull(metrics, "HTTP transport metrcis");
87          this.metrics = metrics;
88          this.buffer = new ByteArrayBuffer(bufferSize);
89          this.fragementSizeHint = fragementSizeHint >= 0 ? fragementSizeHint : 0;
90          this.encoder = charEncoder;
91      }
92  
93      public SessionOutputBufferImpl(
94              final HttpTransportMetricsImpl metrics,
95              final int bufferSize) {
96          this(metrics, bufferSize, bufferSize, null);
97      }
98  
99      public void bind(final OutputStream outStream) {
100         this.outStream = outStream;
101     }
102 
103     public boolean isBound() {
104         return this.outStream != null;
105     }
106 
107     @Override
108     public int capacity() {
109         return this.buffer.capacity();
110     }
111 
112     @Override
113     public int length() {
114         return this.buffer.length();
115     }
116 
117     @Override
118     public int available() {
119         return capacity() - length();
120     }
121 
122     private void streamWrite(final byte[] b, final int off, final int len) throws IOException {
123         Asserts.notNull(outStream, "Output stream");
124         this.outStream.write(b, off, len);
125     }
126 
127     private void flushStream() throws IOException {
128         if (this.outStream != null) {
129             this.outStream.flush();
130         }
131     }
132 
133     private void flushBuffer() throws IOException {
134         final int len = this.buffer.length();
135         if (len > 0) {
136             streamWrite(this.buffer.buffer(), 0, len);
137             this.buffer.clear();
138             this.metrics.incrementBytesTransferred(len);
139         }
140     }
141 
142     @Override
143     public void flush() throws IOException {
144         flushBuffer();
145         flushStream();
146     }
147 
148     @Override
149     public void write(final byte[] b, final int off, final int len) throws IOException {
150         if (b == null) {
151             return;
152         }
153         // Do not want to buffer large-ish chunks
154         // if the byte array is larger then MIN_CHUNK_LIMIT
155         // write it directly to the output stream
156         if (len > this.fragementSizeHint || len > this.buffer.capacity()) {
157             // flush the buffer
158             flushBuffer();
159             // write directly to the out stream
160             streamWrite(b, off, len);
161             this.metrics.incrementBytesTransferred(len);
162         } else {
163             // Do not let the buffer grow unnecessarily
164             final int freecapacity = this.buffer.capacity() - this.buffer.length();
165             if (len > freecapacity) {
166                 // flush the buffer
167                 flushBuffer();
168             }
169             // buffer
170             this.buffer.append(b, off, len);
171         }
172     }
173 
174     @Override
175     public void write(final byte[] b) throws IOException {
176         if (b == null) {
177             return;
178         }
179         write(b, 0, b.length);
180     }
181 
182     @Override
183     public void write(final int b) throws IOException {
184         if (this.fragementSizeHint > 0) {
185             if (this.buffer.isFull()) {
186                 flushBuffer();
187             }
188             this.buffer.append(b);
189         } else {
190             flushBuffer();
191             this.outStream.write(b);
192         }
193     }
194 
195     /**
196      * Writes characters from the specified string followed by a line delimiter
197      * to this session buffer.
198      * <p>
199      * This method uses CR-LF as a line delimiter.
200      *
201      * @param      s   the line.
202      * @throws  IOException  if an I/O error occurs.
203      */
204     @Override
205     public void writeLine(final String s) throws IOException {
206         if (s == null) {
207             return;
208         }
209         if (s.length() > 0) {
210             if (this.encoder == null) {
211                 for (int i = 0; i < s.length(); i++) {
212                     write(s.charAt(i));
213                 }
214             } else {
215                 final CharBuffer cbuf = CharBuffer.wrap(s);
216                 writeEncoded(cbuf);
217             }
218         }
219         write(CRLF);
220     }
221 
222     /**
223      * Writes characters from the specified char array followed by a line
224      * delimiter to this session buffer.
225      * <p>
226      * This method uses CR-LF as a line delimiter.
227      *
228      * @param      charbuffer the buffer containing chars of the line.
229      * @throws  IOException  if an I/O error occurs.
230      */
231     @Override
232     public void writeLine(final CharArrayBuffer charbuffer) throws IOException {
233         if (charbuffer == null) {
234             return;
235         }
236         if (this.encoder == null) {
237             int off = 0;
238             int remaining = charbuffer.length();
239             while (remaining > 0) {
240                 int chunk = this.buffer.capacity() - this.buffer.length();
241                 chunk = Math.min(chunk, remaining);
242                 if (chunk > 0) {
243                     this.buffer.append(charbuffer, off, chunk);
244                 }
245                 if (this.buffer.isFull()) {
246                     flushBuffer();
247                 }
248                 off += chunk;
249                 remaining -= chunk;
250             }
251         } else {
252             final CharBuffer cbuf = CharBuffer.wrap(charbuffer.buffer(), 0, charbuffer.length());
253             writeEncoded(cbuf);
254         }
255         write(CRLF);
256     }
257 
258     private void writeEncoded(final CharBuffer cbuf) throws IOException {
259         if (!cbuf.hasRemaining()) {
260             return;
261         }
262         if (this.bbuf == null) {
263             this.bbuf = ByteBuffer.allocate(1024);
264         }
265         this.encoder.reset();
266         while (cbuf.hasRemaining()) {
267             final CoderResult result = this.encoder.encode(cbuf, this.bbuf, true);
268             handleEncodingResult(result);
269         }
270         final CoderResult result = this.encoder.flush(this.bbuf);
271         handleEncodingResult(result);
272         this.bbuf.clear();
273     }
274 
275     private void handleEncodingResult(final CoderResult result) throws IOException {
276         if (result.isError()) {
277             result.throwException();
278         }
279         this.bbuf.flip();
280         while (this.bbuf.hasRemaining()) {
281             write(this.bbuf.get());
282         }
283         this.bbuf.compact();
284     }
285 
286     @Override
287     public HttpTransportMetrics getMetrics() {
288         return this.metrics;
289     }
290 
291 }