• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.urlconnection;
6 
7 import org.chromium.net.UploadDataProvider;
8 import org.chromium.net.UploadDataSink;
9 
10 import androidx.annotation.VisibleForTesting;
11 
12 import java.io.IOException;
13 import java.net.ProtocolException;
14 import java.nio.ByteBuffer;
15 
16 /**
17  * An implementation of {@link java.io.OutputStream} that buffers entire request
18  * body in memory. This is used when neither
19  * {@link CronetHttpURLConnection#setFixedLengthStreamingMode}
20  * nor {@link CronetHttpURLConnection#setChunkedStreamingMode} is set.
21  */
22 @VisibleForTesting
23 public final class CronetBufferedOutputStream extends CronetOutputStream {
24     // QUIC uses a read buffer of 14520 bytes, SPDY uses 2852 bytes, and normal
25     // stream uses 16384 bytes. Therefore, use 16384 for now to avoid growing
26     // the buffer too many times.
27     private static final int INITIAL_BUFFER_SIZE = 16384;
28     // If content length is not passed in the constructor, this is -1.
29     private final int mInitialContentLength;
30     private final CronetHttpURLConnection mConnection;
31     private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl();
32     // Internal buffer that is used to buffer the request body.
33     private ByteBuffer mBuffer;
34     private boolean mConnected;
35 
36     /**
37      * Package protected constructor.
38      * @param connection The CronetHttpURLConnection object.
39      * @param contentLength The content length of the request body. It must not
40      *            be smaller than 0 or bigger than {@link Integer.MAX_VALUE}.
41      */
CronetBufferedOutputStream(final CronetHttpURLConnection connection, final long contentLength)42     CronetBufferedOutputStream(final CronetHttpURLConnection connection, final long contentLength) {
43         if (connection == null) {
44             throw new NullPointerException("Argument connection cannot be null.");
45         }
46 
47         if (contentLength > Integer.MAX_VALUE) {
48             throw new IllegalArgumentException(
49                     "Use setFixedLengthStreamingMode()"
50                             + " or setChunkedStreamingMode() for requests larger than 2GB.");
51         }
52         if (contentLength < 0) {
53             throw new IllegalArgumentException("Content length < 0.");
54         }
55         mConnection = connection;
56         mInitialContentLength = (int) contentLength;
57         mBuffer = ByteBuffer.allocate(mInitialContentLength);
58     }
59 
60     /**
61      * Package protected constructor used when content length is not known.
62      * @param connection The CronetHttpURLConnection object.
63      */
CronetBufferedOutputStream(final CronetHttpURLConnection connection)64     CronetBufferedOutputStream(final CronetHttpURLConnection connection) {
65         if (connection == null) {
66             throw new NullPointerException();
67         }
68 
69         mConnection = connection;
70         mInitialContentLength = -1;
71         // Buffering without knowing content-length.
72         mBuffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE);
73     }
74 
75     @Override
write(int oneByte)76     public void write(int oneByte) throws IOException {
77         checkNotClosed();
78         ensureCanWrite(1);
79         mBuffer.put((byte) oneByte);
80     }
81 
82     @Override
write(byte[] buffer, int offset, int count)83     public void write(byte[] buffer, int offset, int count) throws IOException {
84         checkNotClosed();
85         ensureCanWrite(count);
86         mBuffer.put(buffer, offset, count);
87     }
88 
89     /** Ensures that {@code count} bytes can be written to the internal buffer. */
ensureCanWrite(int count)90     private void ensureCanWrite(int count) throws IOException {
91         if (mInitialContentLength != -1 && mBuffer.position() + count > mInitialContentLength) {
92             // Error message is to match that of the default implementation.
93             throw new ProtocolException(
94                     "exceeded content-length limit of " + mInitialContentLength + " bytes");
95         }
96         if (mConnected) {
97             throw new IllegalStateException(
98                     "Use setFixedLengthStreamingMode() or "
99                             + "setChunkedStreamingMode() for writing after connect");
100         }
101         if (mInitialContentLength != -1) {
102             // If mInitialContentLength is known, the buffer should not grow.
103             return;
104         }
105         if (mBuffer.limit() - mBuffer.position() > count) {
106             // If there is enough capacity, the buffer should not grow.
107             return;
108         }
109         int afterSize = Math.max(mBuffer.capacity() * 2, mBuffer.capacity() + count);
110         ByteBuffer newByteBuffer = ByteBuffer.allocate(afterSize);
111         mBuffer.flip();
112         newByteBuffer.put(mBuffer);
113         mBuffer = newByteBuffer;
114     }
115 
116     // Below are CronetOutputStream implementations:
117 
118     /** Sets {@link #mConnected} to {@code true}. */
119     @Override
setConnected()120     void setConnected() throws IOException {
121         mConnected = true;
122         if (mBuffer.position() < mInitialContentLength) {
123             throw new ProtocolException("Content received is less than Content-Length");
124         }
125         // Flip the buffer to prepare it for UploadDataProvider read calls.
126         mBuffer.flip();
127     }
128 
129     @Override
checkReceivedEnoughContent()130     void checkReceivedEnoughContent() throws IOException {
131         // Already checked in setConnected. Skip the check here, since mBuffer
132         // might be flipped.
133     }
134 
135     @Override
getUploadDataProvider()136     UploadDataProvider getUploadDataProvider() {
137         return mUploadDataProvider;
138     }
139 
140     private class UploadDataProviderImpl extends UploadDataProvider {
141         @Override
getLength()142         public long getLength() {
143             // This method is supposed to be called just before starting the request.
144             // If content length is not initially passed in, the number of bytes
145             // written will be used as the content length.
146             // TODO(xunjieli): Think of a less fragile way, since getLength() can be
147             // potentially called in other places in the future.
148             if (mInitialContentLength == -1) {
149                 // Account for the fact that setConnected() flip()s mBuffer.
150                 return mConnected ? mBuffer.limit() : mBuffer.position();
151             }
152             return mInitialContentLength;
153         }
154 
155         @Override
read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer)156         public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) {
157             final int availableSpace = byteBuffer.remaining();
158             if (availableSpace < mBuffer.remaining()) {
159                 byteBuffer.put(mBuffer.array(), mBuffer.position(), availableSpace);
160                 mBuffer.position(mBuffer.position() + availableSpace);
161             } else {
162                 byteBuffer.put(mBuffer);
163             }
164             uploadDataSink.onReadSucceeded(false);
165         }
166 
167         @Override
rewind(UploadDataSink uploadDataSink)168         public void rewind(UploadDataSink uploadDataSink) {
169             mBuffer.position(0);
170             uploadDataSink.onRewindSucceeded();
171         }
172     }
173 }
174