• 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 androidx.annotation.VisibleForTesting;
8 
9 import org.chromium.net.UploadDataProvider;
10 import org.chromium.net.UploadDataSink;
11 
12 import androidx.annotation.VisibleForTesting;
13 
14 import java.io.IOException;
15 import java.net.HttpRetryException;
16 import java.net.ProtocolException;
17 import java.nio.ByteBuffer;
18 
19 /**
20  * An implementation of {@link java.io.OutputStream} to send data to a server,
21  * when {@link CronetHttpURLConnection#setFixedLengthStreamingMode} is used.
22  * This implementation does not buffer the entire request body in memory.
23  * It does not support rewind. Note that {@link #write} should only be called
24  * from the thread on which the {@link #mConnection} is created.
25  */
26 @VisibleForTesting
27 public final class CronetFixedModeOutputStream extends CronetOutputStream {
28     // CronetFixedModeOutputStream buffers up to this value and wait for UploadDataStream
29     // to consume the data. This field is non-final, so it can be changed for tests.
30     // Using 16384 bytes is because the internal read buffer is 14520 for QUIC,
31     // 16384 for SPDY, and 16384 for normal HTTP/1.1 stream.
32     @VisibleForTesting public static int sDefaultBufferLength = 16384;
33     private final CronetHttpURLConnection mConnection;
34     private final MessageLoop mMessageLoop;
35     private final long mContentLength;
36     // Internal buffer for holding bytes from the client until the bytes are
37     // copied to the UploadDataSink in UploadDataProvider.read().
38     // CronetFixedModeOutputStream allows client to provide up to
39     // sDefaultBufferLength bytes, and wait for UploadDataProvider.read() to be
40     // called after which point mBuffer is cleared so client can fill in again.
41     // While the client is filling the buffer (via {@code write()}), the buffer's
42     // position points to the next byte to be provided by the client, and limit
43     // points to the end of the buffer. The buffer is flipped before it is
44     // passed to the UploadDataProvider for consuming. Once it is flipped,
45     // buffer position points to the next byte to be copied to the
46     // UploadDataSink, and limit points to the end of data available to be
47     // copied to UploadDataSink. When the UploadDataProvider has provided all
48     // remaining bytes from the buffer to UploadDataSink, it clears the buffer
49     // so client can fill it again.
50     private final ByteBuffer mBuffer;
51     private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl();
52     private long mBytesWritten;
53 
54     /**
55      * Package protected constructor.
56      * @param connection The CronetHttpURLConnection object.
57      * @param contentLength The content length of the request body. Non-zero for
58      *            non-chunked upload.
59      */
CronetFixedModeOutputStream( CronetHttpURLConnection connection, long contentLength, MessageLoop messageLoop)60     CronetFixedModeOutputStream(
61             CronetHttpURLConnection connection, long contentLength, MessageLoop messageLoop) {
62         if (connection == null) {
63             throw new NullPointerException();
64         }
65         if (contentLength < 0) {
66             throw new IllegalArgumentException(
67                     "Content length must be larger than 0 for non-chunked upload.");
68         }
69         mContentLength = contentLength;
70         int bufferSize = (int) Math.min(mContentLength, sDefaultBufferLength);
71         mBuffer = ByteBuffer.allocate(bufferSize);
72         mConnection = connection;
73         mMessageLoop = messageLoop;
74         mBytesWritten = 0;
75     }
76 
77     @Override
write(int oneByte)78     public void write(int oneByte) throws IOException {
79         checkNotClosed();
80         checkNotExceedContentLength(1);
81         ensureBufferHasRemaining();
82         mBuffer.put((byte) oneByte);
83         mBytesWritten++;
84         uploadIfComplete();
85     }
86 
87     @Override
write(byte[] buffer, int offset, int count)88     public void write(byte[] buffer, int offset, int count) throws IOException {
89         checkNotClosed();
90         if (buffer.length - offset < count || offset < 0 || count < 0) {
91             throw new IndexOutOfBoundsException();
92         }
93         checkNotExceedContentLength(count);
94         int toSend = count;
95         while (toSend > 0) {
96             ensureBufferHasRemaining();
97             int sent = Math.min(toSend, mBuffer.remaining());
98             mBuffer.put(buffer, offset + count - toSend, sent);
99             toSend -= sent;
100         }
101         mBytesWritten += count;
102         uploadIfComplete();
103     }
104 
105     /**
106      * If {@code mBuffer} is full, wait until it is consumed and there is
107      * space to write more data to it.
108      */
ensureBufferHasRemaining()109     private void ensureBufferHasRemaining() throws IOException {
110         if (!mBuffer.hasRemaining()) {
111             uploadBufferInternal();
112         }
113     }
114 
115     /**
116      * Waits for the native stack to upload {@code mBuffer}'s contents because
117      * the client has provided all bytes to be uploaded and there is no need to
118      * wait for or expect the client to provide more bytes.
119      */
uploadIfComplete()120     private void uploadIfComplete() throws IOException {
121         if (mBytesWritten == mContentLength) {
122             // Entire post data has been received. Now wait for network stack to
123             // read it.
124             uploadBufferInternal();
125         }
126     }
127 
128     /**
129      * Helper function to upload {@code mBuffer} to the native stack. This
130      * function blocks until {@code mBuffer} is consumed and there is space to
131      * write more data.
132      */
uploadBufferInternal()133     private void uploadBufferInternal() throws IOException {
134         checkNotClosed();
135         mBuffer.flip();
136         mMessageLoop.loop();
137         checkNoException();
138     }
139 
140     /**
141      * Throws {@link java.net.ProtocolException} if adding {@code numBytes} will
142      * exceed content length.
143      */
checkNotExceedContentLength(int numBytes)144     private void checkNotExceedContentLength(int numBytes) throws ProtocolException {
145         if (mBytesWritten + numBytes > mContentLength) {
146             throw new ProtocolException(
147                     "expected "
148                             + (mContentLength - mBytesWritten)
149                             + " bytes but received "
150                             + numBytes);
151         }
152     }
153 
154     // Below are CronetOutputStream implementations:
155 
156     @Override
setConnected()157     void setConnected() throws IOException {
158         // Do nothing.
159     }
160 
161     @Override
checkReceivedEnoughContent()162     void checkReceivedEnoughContent() throws IOException {
163         if (mBytesWritten < mContentLength) {
164             throw new ProtocolException("Content received is less than Content-Length.");
165         }
166     }
167 
168     @Override
getUploadDataProvider()169     UploadDataProvider getUploadDataProvider() {
170         return mUploadDataProvider;
171     }
172 
173     private class UploadDataProviderImpl extends UploadDataProvider {
174         @Override
getLength()175         public long getLength() {
176             return mContentLength;
177         }
178 
179         @Override
read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer)180         public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer) {
181             if (byteBuffer.remaining() >= mBuffer.remaining()) {
182                 byteBuffer.put(mBuffer);
183                 // Reuse this buffer.
184                 mBuffer.clear();
185                 uploadDataSink.onReadSucceeded(false);
186                 // Quit message loop so embedder can write more data.
187                 mMessageLoop.quit();
188             } else {
189                 int oldLimit = mBuffer.limit();
190                 mBuffer.limit(mBuffer.position() + byteBuffer.remaining());
191                 byteBuffer.put(mBuffer);
192                 mBuffer.limit(oldLimit);
193                 uploadDataSink.onReadSucceeded(false);
194             }
195         }
196 
197         @Override
rewind(UploadDataSink uploadDataSink)198         public void rewind(UploadDataSink uploadDataSink) {
199             uploadDataSink.onRewindError(
200                     new HttpRetryException("Cannot retry streamed Http body", -1));
201         }
202     }
203 
204     /**
205      * Sets the default buffer length for use in tests.
206      */
setDefaultBufferLengthForTesting(int length)207     public static void setDefaultBufferLengthForTesting(int length) {
208         sDefaultBufferLength = length;
209     }
210 }
211