Coverage Summary for Class: ReaderInputStream (com.google.common.io)
| Class | Class, % | Method, % | Line, % |
|---|---|---|---|
| ReaderInputStream | 0% (0/1) | 0% (0/10) | 0% (0/68) |
1 /* 2 * Copyright (C) 2015 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.common.io; 16 17 import static com.google.common.base.Preconditions.checkArgument; 18 import static com.google.common.base.Preconditions.checkNotNull; 19 import static com.google.common.base.Preconditions.checkPositionIndexes; 20 21 import com.google.common.annotations.GwtIncompatible; 22 import com.google.common.primitives.UnsignedBytes; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.Reader; 26 import java.nio.Buffer; 27 import java.nio.ByteBuffer; 28 import java.nio.CharBuffer; 29 import java.nio.charset.Charset; 30 import java.nio.charset.CharsetEncoder; 31 import java.nio.charset.CoderResult; 32 import java.nio.charset.CodingErrorAction; 33 import java.util.Arrays; 34 35 /** 36 * An {@link InputStream} that converts characters from a {@link Reader} into bytes using an 37 * arbitrary Charset. 38 * 39 * <p>This is an alternative to copying the data to an {@code OutputStream} via a {@code Writer}, 40 * which is necessarily blocking. By implementing an {@code InputStream} it allows consumers to 41 * "pull" as much data as they can handle, which is more convenient when dealing with flow 42 * controlled, async APIs. 43 * 44 * @author Chris Nokleberg 45 */ 46 @GwtIncompatible 47 @ElementTypesAreNonnullByDefault 48 final class ReaderInputStream extends InputStream { 49 private final Reader reader; 50 private final CharsetEncoder encoder; 51 private final byte[] singleByte = new byte[1]; 52 53 /** 54 * charBuffer holds characters that have been read from the Reader but not encoded yet. The buffer 55 * is perpetually "flipped" (unencoded characters between position and limit). 56 */ 57 private CharBuffer charBuffer; 58 59 /** 60 * byteBuffer holds encoded characters that have not yet been sent to the caller of the input 61 * stream. When encoding it is "unflipped" (encoded bytes between 0 and position) and when 62 * draining it is flipped (undrained bytes between position and limit). 63 */ 64 private ByteBuffer byteBuffer; 65 66 /** Whether we've finished reading the reader. */ 67 private boolean endOfInput; 68 /** Whether we're copying encoded bytes to the caller's buffer. */ 69 private boolean draining; 70 /** Whether we've successfully flushed the encoder. */ 71 private boolean doneFlushing; 72 73 /** 74 * Creates a new input stream that will encode the characters from {@code reader} into bytes using 75 * the given character set. Malformed input and unmappable characters will be replaced. 76 * 77 * @param reader input source 78 * @param charset character set used for encoding chars to bytes 79 * @param bufferSize size of internal input and output buffers 80 * @throws IllegalArgumentException if bufferSize is non-positive 81 */ 82 ReaderInputStream(Reader reader, Charset charset, int bufferSize) { 83 this( 84 reader, 85 charset 86 .newEncoder() 87 .onMalformedInput(CodingErrorAction.REPLACE) 88 .onUnmappableCharacter(CodingErrorAction.REPLACE), 89 bufferSize); 90 } 91 92 /** 93 * Creates a new input stream that will encode the characters from {@code reader} into bytes using 94 * the given character set encoder. 95 * 96 * @param reader input source 97 * @param encoder character set encoder used for encoding chars to bytes 98 * @param bufferSize size of internal input and output buffers 99 * @throws IllegalArgumentException if bufferSize is non-positive 100 */ 101 ReaderInputStream(Reader reader, CharsetEncoder encoder, int bufferSize) { 102 this.reader = checkNotNull(reader); 103 this.encoder = checkNotNull(encoder); 104 checkArgument(bufferSize > 0, "bufferSize must be positive: %s", bufferSize); 105 encoder.reset(); 106 107 charBuffer = CharBuffer.allocate(bufferSize); 108 Java8Compatibility.flip(charBuffer); 109 110 byteBuffer = ByteBuffer.allocate(bufferSize); 111 } 112 113 @Override 114 public void close() throws IOException { 115 reader.close(); 116 } 117 118 @Override 119 public int read() throws IOException { 120 return (read(singleByte) == 1) ? UnsignedBytes.toInt(singleByte[0]) : -1; 121 } 122 123 // TODO(chrisn): Consider trying to encode/flush directly to the argument byte 124 // buffer when possible. 125 @Override 126 public int read(byte[] b, int off, int len) throws IOException { 127 // Obey InputStream contract. 128 checkPositionIndexes(off, off + len, b.length); 129 if (len == 0) { 130 return 0; 131 } 132 133 // The rest of this method implements the process described by the CharsetEncoder javadoc. 134 int totalBytesRead = 0; 135 boolean doneEncoding = endOfInput; 136 137 DRAINING: 138 while (true) { 139 // We stay in draining mode until there are no bytes left in the output buffer. Then we go 140 // back to encoding/flushing. 141 if (draining) { 142 totalBytesRead += drain(b, off + totalBytesRead, len - totalBytesRead); 143 if (totalBytesRead == len || doneFlushing) { 144 return (totalBytesRead > 0) ? totalBytesRead : -1; 145 } 146 draining = false; 147 Java8Compatibility.clear(byteBuffer); 148 } 149 150 while (true) { 151 // We call encode until there is no more input. The last call to encode will have endOfInput 152 // == true. Then there is a final call to flush. 153 CoderResult result; 154 if (doneFlushing) { 155 result = CoderResult.UNDERFLOW; 156 } else if (doneEncoding) { 157 result = encoder.flush(byteBuffer); 158 } else { 159 result = encoder.encode(charBuffer, byteBuffer, endOfInput); 160 } 161 162 if (result.isOverflow()) { 163 // Not enough room in output buffer--drain it, creating a bigger buffer if necessary. 164 startDraining(true); 165 continue DRAINING; 166 } else if (result.isUnderflow()) { 167 // If encoder underflows, it means either: 168 // a) the final flush() succeeded; next drain (then done) 169 // b) we encoded all of the input; next flush 170 // c) we ran of out input to encode; next read more input 171 if (doneEncoding) { // (a) 172 doneFlushing = true; 173 startDraining(false); 174 continue DRAINING; 175 } else if (endOfInput) { // (b) 176 doneEncoding = true; 177 } else { // (c) 178 readMoreChars(); 179 } 180 } else if (result.isError()) { 181 // Only reach here if a CharsetEncoder with non-REPLACE settings is used. 182 result.throwException(); 183 return 0; // Not called. 184 } 185 } 186 } 187 } 188 189 /** Returns a new CharBuffer identical to buf, except twice the capacity. */ 190 private static CharBuffer grow(CharBuffer buf) { 191 char[] copy = Arrays.copyOf(buf.array(), buf.capacity() * 2); 192 CharBuffer bigger = CharBuffer.wrap(copy); 193 Java8Compatibility.position(bigger, buf.position()); 194 Java8Compatibility.limit(bigger, buf.limit()); 195 return bigger; 196 } 197 198 /** Handle the case of underflow caused by needing more input characters. */ 199 private void readMoreChars() throws IOException { 200 // Possibilities: 201 // 1) array has space available on right hand side (between limit and capacity) 202 // 2) array has space available on left hand side (before position) 203 // 3) array has no space available 204 // 205 // In case 2 we shift the existing chars to the left, and in case 3 we create a bigger 206 // array, then they both become case 1. 207 208 if (availableCapacity(charBuffer) == 0) { 209 if (charBuffer.position() > 0) { 210 // (2) There is room in the buffer. Move existing bytes to the beginning. 211 Java8Compatibility.flip(charBuffer.compact()); 212 } else { 213 // (3) Entire buffer is full, need bigger buffer. 214 charBuffer = grow(charBuffer); 215 } 216 } 217 218 // (1) Read more characters into free space at end of array. 219 int limit = charBuffer.limit(); 220 int numChars = reader.read(charBuffer.array(), limit, availableCapacity(charBuffer)); 221 if (numChars == -1) { 222 endOfInput = true; 223 } else { 224 Java8Compatibility.limit(charBuffer, limit + numChars); 225 } 226 } 227 228 /** Returns the number of elements between the limit and capacity. */ 229 private static int availableCapacity(Buffer buffer) { 230 return buffer.capacity() - buffer.limit(); 231 } 232 233 /** 234 * Flips the buffer output buffer so we can start reading bytes from it. If we are starting to 235 * drain because there was overflow, and there aren't actually any characters to drain, then the 236 * overflow must be due to a small output buffer. 237 */ 238 private void startDraining(boolean overflow) { 239 Java8Compatibility.flip(byteBuffer); 240 if (overflow && byteBuffer.remaining() == 0) { 241 byteBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2); 242 } else { 243 draining = true; 244 } 245 } 246 247 /** 248 * Copy as much of the byte buffer into the output array as possible, returning the (positive) 249 * number of characters copied. 250 */ 251 private int drain(byte[] b, int off, int len) { 252 int remaining = Math.min(len, byteBuffer.remaining()); 253 byteBuffer.get(b, off, remaining); 254 return remaining; 255 } 256 }