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 * https://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 package org.apache.commons.io.input;
18
19 import static org.apache.commons.io.IOUtils.EOF;
20
21 import java.io.Reader;
22 import java.io.Serializable;
23 import java.util.Objects;
24
25 /**
26 * {@link Reader} implementation that can read from String, StringBuffer,
27 * StringBuilder or CharBuffer.
28 * <p>
29 * <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
30 * </p>
31 * <h2>Deprecating Serialization</h2>
32 * <p>
33 * <em>Serialization is deprecated and will be removed in 3.0.</em>
34 * </p>
35 *
36 * @since 1.4
37 */
38 public class CharSequenceReader extends Reader implements Serializable {
39
40 private static final long serialVersionUID = 3724187752191401220L;
41
42 /** Source for reading. */
43 private final CharSequence charSequence;
44
45 /** Reading index. */
46 private int idx;
47
48 /** Reader mark. */
49 private int mark;
50
51 /**
52 * The start index in the character sequence, inclusive.
53 * <p>
54 * When de-serializing a CharSequenceReader that was serialized before
55 * this fields was added, this field will be initialized to 0, which
56 * gives the same behavior as before: start reading from the start.
57 * </p>
58 *
59 * @see #start()
60 * @since 2.7
61 */
62 private final int start;
63
64 /**
65 * The end index in the character sequence, exclusive.
66 * <p>
67 * When de-serializing a CharSequenceReader that was serialized before
68 * this fields was added, this field will be initialized to {@code null},
69 * which gives the same behavior as before: stop reading at the
70 * CharSequence's length.
71 * If this field was an int instead, it would be initialized to 0 when the
72 * CharSequenceReader is de-serialized, causing it to not return any
73 * characters at all.
74 * </p>
75 *
76 * @see #end()
77 * @since 2.7
78 */
79 private final Integer end;
80
81 /**
82 * Constructs a new instance with the specified character sequence.
83 *
84 * @param charSequence The character sequence, may be {@code null}
85 */
86 public CharSequenceReader(final CharSequence charSequence) {
87 this(charSequence, 0);
88 }
89
90 /**
91 * Constructs a new instance with a portion of the specified character sequence.
92 * <p>
93 * The start index is not strictly enforced to be within the bounds of the
94 * character sequence. This allows the character sequence to grow or shrink
95 * in size without risking any {@link IndexOutOfBoundsException} to be thrown.
96 * Instead, if the character sequence grows smaller than the start index, this
97 * instance will act as if all characters have been read.
98 * </p>
99 *
100 * @param charSequence The character sequence, may be {@code null}
101 * @param start The start index in the character sequence, inclusive
102 * @throws IllegalArgumentException if the start index is negative
103 * @since 2.7
104 */
105 public CharSequenceReader(final CharSequence charSequence, final int start) {
106 this(charSequence, start, Integer.MAX_VALUE);
107 }
108
109 /**
110 * Constructs a new instance with a portion of the specified character sequence.
111 * <p>
112 * The start and end indexes are not strictly enforced to be within the bounds
113 * of the character sequence. This allows the character sequence to grow or shrink
114 * in size without risking any {@link IndexOutOfBoundsException} to be thrown.
115 * Instead, if the character sequence grows smaller than the start index, this
116 * instance will act as if all characters have been read; if the character sequence
117 * grows smaller than the end, this instance will use the actual character sequence
118 * length.
119 * </p>
120 *
121 * @param charSequence The character sequence, may be {@code null}
122 * @param start The start index in the character sequence, inclusive
123 * @param end The end index in the character sequence, exclusive
124 * @throws IllegalArgumentException if the start index is negative, or if the end index is smaller than the start index
125 * @since 2.7
126 */
127 public CharSequenceReader(final CharSequence charSequence, final int start, final int end) {
128 if (start < 0) {
129 throw new IllegalArgumentException("Start index is less than zero: " + start);
130 }
131 if (end < start) {
132 throw new IllegalArgumentException("End index is less than start " + start + ": " + end);
133 }
134 // Don't check the start and end indexes against the CharSequence,
135 // to let it grow and shrink without breaking existing behavior.
136
137 this.charSequence = charSequence != null ? charSequence : "";
138 this.start = start;
139 this.end = end;
140
141 this.idx = start;
142 this.mark = start;
143 }
144
145 /**
146 * Close resets the file back to the start and removes any marked position.
147 */
148 @Override
149 public void close() {
150 idx = start;
151 mark = start;
152 }
153
154 /**
155 * Returns the index in the character sequence to end reading at, taking into account its length.
156 *
157 * @return The end index in the character sequence (exclusive).
158 */
159 private int end() {
160 /*
161 * end == null for de-serialized instances that were serialized before start and end were added.
162 * Use Integer.MAX_VALUE to get the same behavior as before - use the entire CharSequence.
163 */
164 return Math.min(charSequence.length(), end == null ? Integer.MAX_VALUE : end);
165 }
166
167 /**
168 * Mark the current position.
169 *
170 * @param readAheadLimit ignored
171 */
172 @Override
173 public void mark(final int readAheadLimit) {
174 mark = idx;
175 }
176
177 /**
178 * Mark is supported (returns true).
179 *
180 * @return {@code true}
181 */
182 @Override
183 public boolean markSupported() {
184 return true;
185 }
186
187 /**
188 * Reads a single character.
189 *
190 * @return the next character from the character sequence
191 * or -1 if the end has been reached.
192 */
193 @Override
194 public int read() {
195 if (idx >= end()) {
196 return EOF;
197 }
198 return charSequence.charAt(idx++);
199 }
200
201 /**
202 * Reads the specified number of characters into the array.
203 *
204 * @param array The array to store the characters in
205 * @param offset The starting position in the array to store
206 * @param length The maximum number of characters to read
207 * @return The number of characters read or -1 if there are
208 * no more
209 */
210 @Override
211 public int read(final char[] array, final int offset, final int length) {
212 if (idx >= end()) {
213 return EOF;
214 }
215 Objects.requireNonNull(array, "array");
216 if (length < 0 || offset < 0 || offset + length > array.length) {
217 throw new IndexOutOfBoundsException("Array Size=" + array.length +
218 ", offset=" + offset + ", length=" + length);
219 }
220
221 if (charSequence instanceof String) {
222 final int count = Math.min(length, end() - idx);
223 ((String) charSequence).getChars(idx, idx + count, array, offset);
224 idx += count;
225 return count;
226 }
227 if (charSequence instanceof StringBuilder) {
228 final int count = Math.min(length, end() - idx);
229 ((StringBuilder) charSequence).getChars(idx, idx + count, array, offset);
230 idx += count;
231 return count;
232 }
233 if (charSequence instanceof StringBuffer) {
234 final int count = Math.min(length, end() - idx);
235 ((StringBuffer) charSequence).getChars(idx, idx + count, array, offset);
236 idx += count;
237 return count;
238 }
239
240 int count = 0;
241 for (int i = 0; i < length; i++) {
242 final int c = read();
243 if (c == EOF) {
244 return count;
245 }
246 array[offset + i] = (char) c;
247 count++;
248 }
249 return count;
250 }
251
252 /**
253 * Tells whether this stream is ready to be read.
254 *
255 * @return {@code true} if more characters from the character sequence are available, or {@code false} otherwise.
256 */
257 @Override
258 public boolean ready() {
259 return idx < end();
260 }
261
262 /**
263 * Reset the reader to the last marked position (or the beginning if
264 * mark has not been called).
265 */
266 @Override
267 public void reset() {
268 idx = mark;
269 }
270
271 /**
272 * Skip the specified number of characters.
273 *
274 * @param n The number of characters to skip
275 * @return The actual number of characters skipped
276 */
277 @Override
278 public long skip(final long n) {
279 if (n < 0) {
280 throw new IllegalArgumentException("Number of characters to skip is less than zero: " + n);
281 }
282 if (idx >= end()) {
283 return 0;
284 }
285 final int dest = (int) Math.min(end(), idx + n);
286 final int count = dest - idx;
287 idx = dest;
288 return count;
289 }
290
291 /**
292 * Returns the index in the character sequence to start reading from, taking into account its length.
293 *
294 * @return The start index in the character sequence (inclusive).
295 */
296 private int start() {
297 return Math.min(charSequence.length(), start);
298 }
299
300 /**
301 * Gets a String representation of the underlying
302 * character sequence.
303 *
304 * @return The contents of the character sequence
305 */
306 @Override
307 public String toString() {
308 return charSequence.subSequence(start(), end()).toString();
309 }
310 }