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 }