OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 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.base.VisibleForTesting; |
| 8 import org.chromium.net.UploadDataProvider; |
| 9 import org.chromium.net.UploadDataSink; |
| 10 |
| 11 import java.io.IOException; |
| 12 import java.io.OutputStream; |
| 13 import java.net.ProtocolException; |
| 14 import java.nio.ByteBuffer; |
| 15 |
| 16 /** |
| 17 * An implementation of {@link java.io.OutputStream} to send data to a server, |
| 18 * when {@link CronetHttpURLConnection#setFixedLengthStreamingMode} is used. |
| 19 * This implementation does not buffer the entire request body in memory. |
| 20 * It does not support rewind. Note that {@link #write} should only be called |
| 21 * from the thread on which the {@link #mConnection} is created. |
| 22 */ |
| 23 final class CronetFixedModeOutputStream extends OutputStream |
| 24 implements UploadDataProvider { |
| 25 // CronetFixedModeOutputStream buffers up to this value and wait for UploadD
ataStream |
| 26 // to consume the data. This field is non-final, so it can be changed for te
sts. |
| 27 // TODO(xunjieli): figure out whether this default value should be changed. |
| 28 @VisibleForTesting |
| 29 private static int sDefaultBufferLength = 2048; |
| 30 private final CronetHttpURLConnection mConnection; |
| 31 private final MessageLoop mMessageLoop; |
| 32 private final long mContentLength; |
| 33 private final ByteBuffer mBuffer; |
| 34 private long mBytesReceived; |
| 35 |
| 36 /** |
| 37 * Package protected constructor. |
| 38 * @param connection The CronetHttpURLConnection object. |
| 39 * @param contentLength The content length of the request body. Non-zero for |
| 40 * non-chunked upload. |
| 41 */ |
| 42 CronetFixedModeOutputStream(CronetHttpURLConnection connection, |
| 43 long contentLength, MessageLoop messageLoop) { |
| 44 if (connection == null) { |
| 45 throw new NullPointerException(); |
| 46 } |
| 47 if (contentLength < 0) { |
| 48 throw new IllegalArgumentException( |
| 49 "Content length must be larger than 0 for non-chunked upload
."); |
| 50 } |
| 51 mContentLength = contentLength; |
| 52 int bufferSize = (int) Math.min(mContentLength, sDefaultBufferLength); |
| 53 mBuffer = ByteBuffer.allocate(bufferSize); |
| 54 mConnection = connection; |
| 55 mMessageLoop = messageLoop; |
| 56 mBytesReceived = 0; |
| 57 } |
| 58 |
| 59 @Override |
| 60 public void write(int oneByte) throws IOException { |
| 61 checkNotExceedContentLength(1); |
| 62 while (mBuffer.position() == mBuffer.limit()) { |
| 63 // Wait until buffer is consumed. |
| 64 mMessageLoop.loop(); |
| 65 } |
| 66 mBuffer.put((byte) oneByte); |
| 67 mBytesReceived++; |
| 68 if (mBytesReceived == mContentLength) { |
| 69 // Entire post data has been received. Now wait for network stack to |
| 70 // read it. |
| 71 mMessageLoop.loop(); |
| 72 } |
| 73 } |
| 74 |
| 75 @Override |
| 76 public void write(byte[] buffer, int offset, int count) throws IOException { |
| 77 if (buffer.length - offset < count || offset < 0 || count < 0) { |
| 78 throw new IndexOutOfBoundsException(); |
| 79 } |
| 80 checkNotExceedContentLength(count); |
| 81 if (count == 0) { |
| 82 return; |
| 83 } |
| 84 int toSend = count; |
| 85 while (toSend > 0) { |
| 86 if (mBuffer.position() == mBuffer.limit()) { |
| 87 // Wait until buffer is consumed. |
| 88 mMessageLoop.loop(); |
| 89 } |
| 90 int sent = Math.min(toSend, mBuffer.limit() - mBuffer.position()); |
| 91 mBuffer.put(buffer, offset + count - toSend, sent); |
| 92 toSend -= sent; |
| 93 } |
| 94 mBytesReceived += count; |
| 95 if (mBytesReceived == mContentLength) { |
| 96 // Entire post data has been received. Now wait for network stack to |
| 97 // read it. |
| 98 mMessageLoop.loop(); |
| 99 } |
| 100 } |
| 101 |
| 102 // TODO(xunjieli): implement close(). |
| 103 |
| 104 /** |
| 105 * Throws {@link java.net.ProtocolException} if adding {@code numBytes} will |
| 106 * exceed content length. |
| 107 */ |
| 108 private void checkNotExceedContentLength(int numBytes) throws ProtocolExcept
ion { |
| 109 if (mContentLength != -1 && mBytesReceived + numBytes > mContentLength)
{ |
| 110 throw new ProtocolException("expected " |
| 111 + (mContentLength - mBytesReceived) + " bytes but received " |
| 112 + numBytes); |
| 113 } |
| 114 } |
| 115 |
| 116 // Below are UploadDataProvider implementations. Only intended to be used |
| 117 // within Cronet. |
| 118 |
| 119 @Override |
| 120 public long getLength() { |
| 121 return mContentLength; |
| 122 } |
| 123 |
| 124 @Override |
| 125 public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteB
uffer) { |
| 126 int availableSpace = byteBuffer.capacity() - byteBuffer.position(); |
| 127 if (availableSpace < mBuffer.position()) { |
| 128 // byteBuffer does not have enough capacity, so only put a portion |
| 129 // of mBuffer in it. |
| 130 byteBuffer.put(mBuffer.array(), 0, availableSpace); |
| 131 mBuffer.position(availableSpace); |
| 132 // Move remaining buffer to the head of the buffer for use in the |
| 133 // next read call. |
| 134 mBuffer.compact(); |
| 135 } else { |
| 136 // byteBuffer has enough capacity to hold the content of mBuffer. |
| 137 mBuffer.flip(); |
| 138 byteBuffer.put(mBuffer); |
| 139 // Reuse this buffer. |
| 140 mBuffer.clear(); |
| 141 // Quit message loop so embedder can write more data. |
| 142 mMessageLoop.postQuitTask(); |
| 143 } |
| 144 uploadDataSink.onReadSucceeded(false); |
| 145 } |
| 146 |
| 147 @Override |
| 148 public void rewind(UploadDataSink uploadDataSink) { |
| 149 uploadDataSink.onRewindError(new IllegalStateException( |
| 150 "Rewind is not supported by CronetFixedModeOutputStream.")); |
| 151 } |
| 152 |
| 153 /** |
| 154 * Sets the default buffer length for use in tests. |
| 155 */ |
| 156 @VisibleForTesting |
| 157 static void setDefaultBufferLengthForTesting(int length) { |
| 158 sDefaultBufferLength = length; |
| 159 } |
| 160 } |
OLD | NEW |