001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.io.input; 018 019import static org.apache.commons.io.IOUtils.EOF; 020 021import java.io.Reader; 022import java.io.Serializable; 023import java.util.Objects; 024 025/** 026 * {@link Reader} implementation that can read from String, StringBuffer, 027 * StringBuilder or CharBuffer. 028 * <p> 029 * <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}. 030 * </p> 031 * 032 * @since 1.4 033 */ 034public class CharSequenceReader extends Reader implements Serializable { 035 036 private static final long serialVersionUID = 3724187752191401220L; 037 private final CharSequence charSequence; 038 private int idx; 039 private int mark; 040 041 /** 042 * The start index in the character sequence, inclusive. 043 * <p> 044 * When de-serializing a CharSequenceReader that was serialized before 045 * this fields was added, this field will be initialized to 0, which 046 * gives the same behavior as before: start reading from the start. 047 * </p> 048 * 049 * @see #start() 050 * @since 2.7 051 */ 052 private final int start; 053 054 /** 055 * The end index in the character sequence, exclusive. 056 * <p> 057 * When de-serializing a CharSequenceReader that was serialized before 058 * this fields was added, this field will be initialized to {@code null}, 059 * which gives the same behavior as before: stop reading at the 060 * CharSequence's length. 061 * If this field was an int instead, it would be initialized to 0 when the 062 * CharSequenceReader is de-serialized, causing it to not return any 063 * characters at all. 064 * </p> 065 * 066 * @see #end() 067 * @since 2.7 068 */ 069 private final Integer end; 070 071 /** 072 * Constructs a new instance with the specified character sequence. 073 * 074 * @param charSequence The character sequence, may be {@code null} 075 */ 076 public CharSequenceReader(final CharSequence charSequence) { 077 this(charSequence, 0); 078 } 079 080 /** 081 * Constructs a new instance with a portion of the specified character sequence. 082 * <p> 083 * The start index is not strictly enforced to be within the bounds of the 084 * character sequence. This allows the character sequence to grow or shrink 085 * in size without risking any {@link IndexOutOfBoundsException} to be thrown. 086 * Instead, if the character sequence grows smaller than the start index, this 087 * instance will act as if all characters have been read. 088 * </p> 089 * 090 * @param charSequence The character sequence, may be {@code null} 091 * @param start The start index in the character sequence, inclusive 092 * @throws IllegalArgumentException if the start index is negative 093 * @since 2.7 094 */ 095 public CharSequenceReader(final CharSequence charSequence, final int start) { 096 this(charSequence, start, Integer.MAX_VALUE); 097 } 098 099 /** 100 * Constructs a new instance with a portion of the specified character sequence. 101 * <p> 102 * The start and end indexes are not strictly enforced to be within the bounds 103 * of the character sequence. This allows the character sequence to grow or shrink 104 * in size without risking any {@link IndexOutOfBoundsException} to be thrown. 105 * Instead, if the character sequence grows smaller than the start index, this 106 * instance will act as if all characters have been read; if the character sequence 107 * grows smaller than the end, this instance will use the actual character sequence 108 * length. 109 * </p> 110 * 111 * @param charSequence The character sequence, may be {@code null} 112 * @param start The start index in the character sequence, inclusive 113 * @param end The end index in the character sequence, exclusive 114 * @throws IllegalArgumentException if the start index is negative, or if the end index is smaller than the start index 115 * @since 2.7 116 */ 117 public CharSequenceReader(final CharSequence charSequence, final int start, final int end) { 118 if (start < 0) { 119 throw new IllegalArgumentException( 120 "Start index is less than zero: " + start); 121 } 122 if (end < start) { 123 throw new IllegalArgumentException( 124 "End index is less than start " + start + ": " + end); 125 } 126 // Don't check the start and end indexes against the CharSequence, 127 // to let it grow and shrink without breaking existing behavior. 128 129 this.charSequence = charSequence != null ? charSequence : ""; 130 this.start = start; 131 this.end = end; 132 133 this.idx = start; 134 this.mark = start; 135 } 136 137 /** 138 * Returns the index in the character sequence to start reading from, taking into account its length. 139 * 140 * @return The start index in the character sequence (inclusive). 141 */ 142 private int start() { 143 return Math.min(charSequence.length(), start); 144 } 145 146 /** 147 * Returns the index in the character sequence to end reading at, taking into account its length. 148 * 149 * @return The end index in the character sequence (exclusive). 150 */ 151 private int end() { 152 /* 153 * end == null for de-serialized instances that were serialized before start and end were added. 154 * Use Integer.MAX_VALUE to get the same behavior as before - use the entire CharSequence. 155 */ 156 return Math.min(charSequence.length(), end == null ? Integer.MAX_VALUE : end); 157 } 158 159 /** 160 * Close resets the file back to the start and removes any marked position. 161 */ 162 @Override 163 public void close() { 164 idx = start; 165 mark = start; 166 } 167 168 /** 169 * Tells whether this stream is ready to be read. 170 * 171 * @return {@code true} if more characters from the character sequence are available, or {@code false} otherwise. 172 */ 173 @Override 174 public boolean ready() { 175 return idx < end(); 176 } 177 178 /** 179 * Mark the current position. 180 * 181 * @param readAheadLimit ignored 182 */ 183 @Override 184 public void mark(final int readAheadLimit) { 185 mark = idx; 186 } 187 188 /** 189 * Mark is supported (returns true). 190 * 191 * @return {@code true} 192 */ 193 @Override 194 public boolean markSupported() { 195 return true; 196 } 197 198 /** 199 * Read a single character. 200 * 201 * @return the next character from the character sequence 202 * or -1 if the end has been reached. 203 */ 204 @Override 205 public int read() { 206 if (idx >= end()) { 207 return EOF; 208 } 209 return charSequence.charAt(idx++); 210 } 211 212 /** 213 * Read the specified number of characters into the array. 214 * 215 * @param array The array to store the characters in 216 * @param offset The starting position in the array to store 217 * @param length The maximum number of characters to read 218 * @return The number of characters read or -1 if there are 219 * no more 220 */ 221 @Override 222 public int read(final char[] array, final int offset, final int length) { 223 if (idx >= end()) { 224 return EOF; 225 } 226 Objects.requireNonNull(array, "array"); 227 if (length < 0 || offset < 0 || offset + length > array.length) { 228 throw new IndexOutOfBoundsException("Array Size=" + array.length + 229 ", offset=" + offset + ", length=" + length); 230 } 231 232 if (charSequence instanceof String) { 233 final int count = Math.min(length, end() - idx); 234 ((String) charSequence).getChars(idx, idx + count, array, offset); 235 idx += count; 236 return count; 237 } 238 if (charSequence instanceof StringBuilder) { 239 final int count = Math.min(length, end() - idx); 240 ((StringBuilder) charSequence).getChars(idx, idx + count, array, offset); 241 idx += count; 242 return count; 243 } 244 if (charSequence instanceof StringBuffer) { 245 final int count = Math.min(length, end() - idx); 246 ((StringBuffer) charSequence).getChars(idx, idx + count, array, offset); 247 idx += count; 248 return count; 249 } 250 251 int count = 0; 252 for (int i = 0; i < length; i++) { 253 final int c = read(); 254 if (c == EOF) { 255 return count; 256 } 257 array[offset + i] = (char)c; 258 count++; 259 } 260 return count; 261 } 262 263 /** 264 * Reset the reader to the last marked position (or the beginning if 265 * mark has not been called). 266 */ 267 @Override 268 public void reset() { 269 idx = mark; 270 } 271 272 /** 273 * Skip the specified number of characters. 274 * 275 * @param n The number of characters to skip 276 * @return The actual number of characters skipped 277 */ 278 @Override 279 public long skip(final long n) { 280 if (n < 0) { 281 throw new IllegalArgumentException( 282 "Number of characters to skip is less than zero: " + n); 283 } 284 if (idx >= end()) { 285 return 0; 286 } 287 final int dest = (int)Math.min(end(), idx + n); 288 final int count = dest - idx; 289 idx = dest; 290 return count; 291 } 292 293 /** 294 * Return a String representation of the underlying 295 * character sequence. 296 * 297 * @return The contents of the character sequence 298 */ 299 @Override 300 public String toString() { 301 final CharSequence subSequence = charSequence.subSequence(start(), end()); 302 return subSequence.toString(); 303 } 304}