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 * Mark the current position. 170 * 171 * @param readAheadLimit ignored 172 */ 173 @Override 174 public void mark(final int readAheadLimit) { 175 mark = idx; 176 } 177 178 /** 179 * Mark is supported (returns true). 180 * 181 * @return {@code true} 182 */ 183 @Override 184 public boolean markSupported() { 185 return true; 186 } 187 188 /** 189 * Read a single character. 190 * 191 * @return the next character from the character sequence 192 * or -1 if the end has been reached. 193 */ 194 @Override 195 public int read() { 196 if (idx >= end()) { 197 return EOF; 198 } 199 return charSequence.charAt(idx++); 200 } 201 202 /** 203 * Read the specified number of characters into the array. 204 * 205 * @param array The array to store the characters in 206 * @param offset The starting position in the array to store 207 * @param length The maximum number of characters to read 208 * @return The number of characters read or -1 if there are 209 * no more 210 */ 211 @Override 212 public int read(final char[] array, final int offset, final int length) { 213 if (idx >= end()) { 214 return EOF; 215 } 216 Objects.requireNonNull(array, "array"); 217 if (length < 0 || offset < 0 || offset + length > array.length) { 218 throw new IndexOutOfBoundsException("Array Size=" + array.length + 219 ", offset=" + offset + ", length=" + length); 220 } 221 222 if (charSequence instanceof String) { 223 final int count = Math.min(length, end() - idx); 224 ((String) charSequence).getChars(idx, idx + count, array, offset); 225 idx += count; 226 return count; 227 } 228 if (charSequence instanceof StringBuilder) { 229 final int count = Math.min(length, end() - idx); 230 ((StringBuilder) charSequence).getChars(idx, idx + count, array, offset); 231 idx += count; 232 return count; 233 } 234 if (charSequence instanceof StringBuffer) { 235 final int count = Math.min(length, end() - idx); 236 ((StringBuffer) charSequence).getChars(idx, idx + count, array, offset); 237 idx += count; 238 return count; 239 } 240 241 int count = 0; 242 for (int i = 0; i < length; i++) { 243 final int c = read(); 244 if (c == EOF) { 245 return count; 246 } 247 array[offset + i] = (char)c; 248 count++; 249 } 250 return count; 251 } 252 253 /** 254 * Reset the reader to the last marked position (or the beginning if 255 * mark has not been called). 256 */ 257 @Override 258 public void reset() { 259 idx = mark; 260 } 261 262 /** 263 * Skip the specified number of characters. 264 * 265 * @param n The number of characters to skip 266 * @return The actual number of characters skipped 267 */ 268 @Override 269 public long skip(final long n) { 270 if (n < 0) { 271 throw new IllegalArgumentException( 272 "Number of characters to skip is less than zero: " + n); 273 } 274 if (idx >= end()) { 275 return EOF; 276 } 277 final int dest = (int)Math.min(end(), idx + n); 278 final int count = dest - idx; 279 idx = dest; 280 return count; 281 } 282 283 /** 284 * Return a String representation of the underlying 285 * character sequence. 286 * 287 * @return The contents of the character sequence 288 */ 289 @Override 290 public String toString() { 291 CharSequence subSequence = charSequence.subSequence(start(), end()); 292 return subSequence.toString(); 293 } 294}