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