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;
032import java.nio.ByteBuffer;
033import java.nio.CharBuffer;
034import java.nio.charset.CharsetEncoder;
035import java.nio.charset.CoderResult;
036
037import org.apache.http.io.BufferInfo;
038import org.apache.http.io.HttpTransportMetrics;
039import org.apache.http.io.SessionOutputBuffer;
040import org.apache.http.protocol.HTTP;
041import org.apache.http.util.Args;
042import org.apache.http.util.Asserts;
043import org.apache.http.util.ByteArrayBuffer;
044import org.apache.http.util.CharArrayBuffer;
045
046/**
047 * Abstract base class for session output buffers that stream data to
048 * an arbitrary {@link OutputStream}. This class buffers small chunks of
049 * output data in an internal byte array for optimal output performance.
050 * <p>
051 * {@link #writeLine(CharArrayBuffer)} and {@link #writeLine(String)} methods
052 * of this class use CR-LF as a line delimiter.
053 *
054 * @since 4.3
055 */
056public class SessionOutputBufferImpl implements SessionOutputBuffer, BufferInfo {
057
058    private static final byte[] CRLF = new byte[] {HTTP.CR, HTTP.LF};
059
060    private final HttpTransportMetricsImpl metrics;
061    private final ByteArrayBuffer buffer;
062    private final int fragementSizeHint;
063    private final CharsetEncoder encoder;
064
065    private OutputStream outstream;
066    private ByteBuffer bbuf;
067
068    /**
069     * Creates new instance of SessionOutputBufferImpl.
070     *
071     * @param metrics HTTP transport metrics.
072     * @param buffersize buffer size. Must be a positive number.
073     * @param fragementSizeHint fragment size hint defining a minimal size of a fragment
074     *   that should be written out directly to the socket bypassing the session buffer.
075     *   Value {@code 0} disables fragment buffering.
076     * @param charencoder charencoder to be used for encoding HTTP protocol elements.
077     *   If {@code null} simple type cast will be used for char to byte conversion.
078     */
079    public SessionOutputBufferImpl(
080            final HttpTransportMetricsImpl metrics,
081            final int buffersize,
082            final int fragementSizeHint,
083            final CharsetEncoder charencoder) {
084        super();
085        Args.positive(buffersize, "Buffer size");
086        Args.notNull(metrics, "HTTP transport metrcis");
087        this.metrics = metrics;
088        this.buffer = new ByteArrayBuffer(buffersize);
089        this.fragementSizeHint = fragementSizeHint >= 0 ? fragementSizeHint : 0;
090        this.encoder = charencoder;
091    }
092
093    public SessionOutputBufferImpl(
094            final HttpTransportMetricsImpl metrics,
095            final int buffersize) {
096        this(metrics, buffersize, buffersize, null);
097    }
098
099    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}