Coverage Report - org.apache.commons.io.input.CharSequenceInputStream
 
Classes in this File Line Coverage Branch Coverage Complexity
CharSequenceInputStream
91%
73/80
73%
34/46
3.214
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 
 18  
 package org.apache.commons.io.input;
 19  
 
 20  
 import java.io.IOException;
 21  
 import java.io.InputStream;
 22  
 import java.nio.ByteBuffer;
 23  
 import java.nio.CharBuffer;
 24  
 import java.nio.charset.CharacterCodingException;
 25  
 import java.nio.charset.Charset;
 26  
 import java.nio.charset.CharsetEncoder;
 27  
 import java.nio.charset.CoderResult;
 28  
 import java.nio.charset.CodingErrorAction;
 29  
 
 30  
 /**
 31  
  * {@link InputStream} implementation that can read from String, StringBuffer,
 32  
  * StringBuilder or CharBuffer.
 33  
  * <p>
 34  
  * <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
 35  
  *
 36  
  * @since 2.2
 37  
  */
 38  
 public class CharSequenceInputStream extends InputStream {
 39  
 
 40  
     private static final int BUFFER_SIZE = 2048;
 41  
 
 42  
     private static final int EOS = -1;
 43  
 
 44  
     private static final int NO_MARK = -1;
 45  
 
 46  
     private final CharsetEncoder encoder;
 47  
     private final CharBuffer cbuf;
 48  
     private final ByteBuffer bbuf;
 49  
 
 50  
     private int mark_cbuf; // position in cbuf
 51  
     private int mark_bbuf; // position in bbuf
 52  
 
 53  
     /**
 54  
      * Constructor.
 55  
      *
 56  
      * @param cs the input character sequence
 57  
      * @param charset the character set name to use
 58  
      * @param bufferSize the buffer size to use.
 59  
      * @throws IllegalArgumentException if the buffer is not large enough to hold a complete character
 60  
      */
 61  
     public CharSequenceInputStream(final CharSequence cs, final Charset charset, final int bufferSize) {
 62  1664
         super();
 63  1664
         this.encoder = charset.newEncoder()
 64  
             .onMalformedInput(CodingErrorAction.REPLACE)
 65  
             .onUnmappableCharacter(CodingErrorAction.REPLACE);
 66  
         // Ensure that buffer is long enough to hold a complete character
 67  1664
         final float maxBytesPerChar = encoder.maxBytesPerChar();
 68  1664
         if (bufferSize < maxBytesPerChar) {
 69  0
             throw new IllegalArgumentException("Buffer size " + bufferSize + " is less than maxBytesPerChar " + maxBytesPerChar);
 70  
         }
 71  1664
         this.bbuf = ByteBuffer.allocate(bufferSize);
 72  1664
         this.bbuf.flip();
 73  1664
         this.cbuf = CharBuffer.wrap(cs);
 74  1664
         this.mark_cbuf = NO_MARK;
 75  1664
         this.mark_bbuf = NO_MARK;
 76  1664
     }
 77  
 
 78  
     /**
 79  
      * Constructor, calls {@link #CharSequenceInputStream(CharSequence, Charset, int)}.
 80  
      *
 81  
      * @param cs the input character sequence
 82  
      * @param charset the character set name to use
 83  
      * @param bufferSize the buffer size to use.
 84  
      * @throws IllegalArgumentException if the buffer is not large enough to hold a complete character
 85  
      */
 86  
     public CharSequenceInputStream(final CharSequence cs, final String charset, final int bufferSize) {
 87  1652
         this(cs, Charset.forName(charset), bufferSize);
 88  1652
     }
 89  
 
 90  
     /**
 91  
      * Constructor, calls {@link #CharSequenceInputStream(CharSequence, Charset, int)}
 92  
      * with a buffer size of 2048.
 93  
      *
 94  
      * @param cs the input character sequence
 95  
      * @param charset the character set name to use
 96  
      * @throws IllegalArgumentException if the buffer is not large enough to hold a complete character
 97  
      */
 98  
     public CharSequenceInputStream(final CharSequence cs, final Charset charset) {
 99  0
         this(cs, charset, BUFFER_SIZE);
 100  0
     }
 101  
 
 102  
     /**
 103  
      * Constructor, calls {@link #CharSequenceInputStream(CharSequence, String, int)}
 104  
      * with a buffer size of 2048.
 105  
      *
 106  
      * @param cs the input character sequence
 107  
      * @param charset the character set name to use
 108  
      * @throws IllegalArgumentException if the buffer is not large enough to hold a complete character
 109  
      */
 110  
     public CharSequenceInputStream(final CharSequence cs, final String charset) {
 111  688
         this(cs, charset, BUFFER_SIZE);
 112  688
     }
 113  
 
 114  
     /**
 115  
      * Fills the byte output buffer from the input char buffer.
 116  
      *
 117  
      * @throws CharacterCodingException
 118  
      *             an error encoding data
 119  
      */
 120  
     private void fillBuffer() throws CharacterCodingException {
 121  4278
         this.bbuf.compact();
 122  4278
         final CoderResult result = this.encoder.encode(this.cbuf, this.bbuf, true);
 123  4278
         if (result.isError()) {
 124  0
             result.throwException();
 125  
         }
 126  4278
         this.bbuf.flip();
 127  4278
     }
 128  
 
 129  
     @Override
 130  
     public int read(final byte[] b, int off, int len) throws IOException {
 131  5555
         if (b == null) {
 132  0
             throw new NullPointerException("Byte array is null");
 133  
         }
 134  5555
         if (len < 0 || (off + len) > b.length) {
 135  0
             throw new IndexOutOfBoundsException("Array Size=" + b.length +
 136  
                     ", offset=" + off + ", length=" + len);
 137  
         }
 138  5555
         if (len == 0) {
 139  80
             return 0; // must return 0 for zero length read
 140  
         }
 141  5475
         if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
 142  360
             return EOS;
 143  
         }
 144  5115
         int bytesRead = 0;
 145  14678
         while (len > 0) {
 146  9913
             if (this.bbuf.hasRemaining()) {
 147  6589
                 final int chunk = Math.min(this.bbuf.remaining(), len);
 148  6589
                 this.bbuf.get(b, off, chunk);
 149  6589
                 off += chunk;
 150  6589
                 len -= chunk;
 151  6589
                 bytesRead += chunk;
 152  6589
             } else {
 153  3324
                 fillBuffer();
 154  3324
                 if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
 155  350
                     break;
 156  
                 }
 157  
             }
 158  
         }
 159  5115
         return bytesRead == 0 && !this.cbuf.hasRemaining() ? EOS : bytesRead;
 160  
     }
 161  
 
 162  
     @Override
 163  
     public int read() throws IOException {
 164  
         for (;;) {
 165  95684
             if (this.bbuf.hasRemaining()) {
 166  94740
                 return this.bbuf.get() & 0xFF;
 167  
             } else {
 168  944
                 fillBuffer();
 169  944
                 if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
 170  54
                     return EOS;
 171  
                 }
 172  
             }
 173  
         }
 174  
     }
 175  
 
 176  
     @Override
 177  
     public int read(final byte[] b) throws IOException {
 178  1148
         return read(b, 0, b.length);
 179  
     }
 180  
 
 181  
     @Override
 182  
     public long skip(long n) throws IOException {
 183  
         /*
 184  
          * This could be made more efficient by using position to skip within the current buffer.
 185  
          */
 186  1012
         long skipped = 0;
 187  3356
         while (n > 0 && available() > 0) {
 188  2344
             this.read();
 189  2344
             n--;
 190  2344
             skipped++;
 191  
         }
 192  1012
         return skipped;
 193  
     }
 194  
 
 195  
     /**
 196  
      * Return an estimate of the number of bytes remaining in the byte stream.
 197  
      * @return the count of bytes that can be read without blocking (or returning EOF).
 198  
      *
 199  
      * @throws IOException if an error occurs (probably not possible)
 200  
      */
 201  
     @Override
 202  
     public int available() throws IOException {
 203  
         // The cached entries are in bbuf; since encoding always creates at least one byte
 204  
         // per character, we can add the two to get a better estimate (e.g. if bbuf is empty)
 205  
         // Note that the previous implementation (2.4) could return zero even though there were
 206  
         // encoded bytes still available.
 207  4008
         return this.bbuf.remaining() + this.cbuf.remaining();
 208  
     }
 209  
 
 210  
     @Override
 211  
     public void close() throws IOException {
 212  1664
     }
 213  
 
 214  
     /**
 215  
      * {@inheritDoc}
 216  
      * @param readlimit max read limit (ignored)
 217  
      */
 218  
     @Override
 219  
     public synchronized void mark(final int readlimit) {
 220  578
         this.mark_cbuf = this.cbuf.position();
 221  578
         this.mark_bbuf = this.bbuf.position();
 222  578
         this.cbuf.mark();
 223  578
         this.bbuf.mark();
 224  
         // It would be nice to be able to use mark & reset on the cbuf and bbuf;
 225  
         // however the bbuf is re-used so that won't work
 226  578
     }
 227  
 
 228  
     @Override
 229  
     public synchronized void reset() throws IOException {
 230  
         /*
 231  
          * This is not the most efficient implementation, as it re-encodes from the beginning.
 232  
          *
 233  
          * Since the bbuf is re-used, in general it's necessary to re-encode the data.
 234  
          *
 235  
          * It should be possible to apply some optimisations however:
 236  
          * + use mark/reset on the cbuf and bbuf. This would only work if the buffer had not been (re)filled since the mark.
 237  
          * The code would have to catch InvalidMarkException - does not seem possible to check if mark is valid otherwise.
 238  
          * + Try saving the state of the cbuf before each fillBuffer; it might be possible to restart from there.
 239  
          */
 240  586
         if (this.mark_cbuf != NO_MARK) {
 241  
             // if cbuf is at 0, we have not started reading anything, so skip re-encoding
 242  578
             if (this.cbuf.position() != 0) {
 243  578
                 this.encoder.reset();
 244  578
                 this.cbuf.rewind();
 245  578
                 this.bbuf.rewind();
 246  578
                 this.bbuf.limit(0); // rewind does not clear the buffer
 247  588
                 while(this.cbuf.position() < this.mark_cbuf) {
 248  10
                     this.bbuf.rewind(); // empty the buffer (we only refill when empty during normal processing)
 249  10
                     this.bbuf.limit(0);
 250  10
                     fillBuffer();
 251  
                 }
 252  
             }
 253  578
             if (this.cbuf.position() != this.mark_cbuf) {
 254  0
                 throw new IllegalStateException("Unexpected CharBuffer postion: actual="+cbuf.position() + " expected=" + this.mark_cbuf);
 255  
             }
 256  578
             this.bbuf.position(this.mark_bbuf);
 257  578
             this.mark_cbuf = NO_MARK;
 258  578
             this.mark_bbuf = NO_MARK;
 259  
         }
 260  586
     }
 261  
 
 262  
     @Override
 263  
     public boolean markSupported() {
 264  2
         return true;
 265  
     }
 266  
 
 267  
 }