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
24 import org.apache.commons.io.IOUtils;
25
26 /**
27 * {@link Reader} implementation that can read from String, StringBuffer,
28 * StringBuilder or CharBuffer.
29 * <p>
30 * <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
31 * </p>
32 * <h2>Deprecating Serialization</h2>
33 * <p>
34 * <em>Serialization is deprecated and will be removed in 3.0.</em>
35 * </p>
36 *
37 * @since 1.4
38 */
39 public class CharSequenceReader extends Reader implements Serializable {
40
41 private static final long serialVersionUID = 3724187752191401220L;
42
43 /** Source for reading. */
44 private final CharSequence charSequence;
45
46 /** Reading index. */
47 private int idx;
48
49 /** Reader mark. */
50 private int mark;
51
52 /**
53 * The start index in the character sequence, inclusive.
54 * <p>
55 * When de-serializing a CharSequenceReader that was serialized before
56 * this fields was added, this field will be initialized to 0, which
57 * gives the same behavior as before: start reading from the start.
58 * </p>
59 *
60 * @see #start()
61 * @since 2.7
62 */
63 private final int start;
64
65 /**
66 * The end index in the character sequence, exclusive.
67 * <p>
68 * When de-serializing a CharSequenceReader that was serialized before
69 * this fields was added, this field will be initialized to {@code null},
70 * which gives the same behavior as before: stop reading at the
71 * CharSequence's length.
72 * If this field was an int instead, it would be initialized to 0 when the
73 * CharSequenceReader is de-serialized, causing it to not return any
74 * characters at all.
75 * </p>
76 *
77 * @see #end()
78 * @since 2.7
79 */
80 private final Integer end;
81
82 /**
83 * Constructs a new instance with the specified character sequence.
84 *
85 * @param charSequence The character sequence, may be {@code null}
86 */
87 public CharSequenceReader(final CharSequence charSequence) {
88 this(charSequence, 0);
89 }
90
91 /**
92 * Constructs a new instance with a portion of the specified character sequence.
93 * <p>
94 * The start index is not strictly enforced to be within the bounds of the
95 * character sequence. This allows the character sequence to grow or shrink
96 * in size without risking any {@link IndexOutOfBoundsException} to be thrown.
97 * Instead, if the character sequence grows smaller than the start index, this
98 * instance will act as if all characters have been read.
99 * </p>
100 *
101 * @param charSequence The character sequence, may be {@code null}
102 * @param start The start index in the character sequence, inclusive
103 * @throws IllegalArgumentException if the start index is negative
104 * @since 2.7
105 */
106 public CharSequenceReader(final CharSequence charSequence, final int start) {
107 this(charSequence, start, Integer.MAX_VALUE);
108 }
109
110 /**
111 * Constructs a new instance with a portion of the specified character sequence.
112 * <p>
113 * The start and end indexes are not strictly enforced to be within the bounds
114 * of the character sequence. This allows the character sequence to grow or shrink
115 * in size without risking any {@link IndexOutOfBoundsException} to be thrown.
116 * Instead, if the character sequence grows smaller than the start index, this
117 * instance will act as if all characters have been read; if the character sequence
118 * grows smaller than the end, this instance will use the actual character sequence
119 * length.
120 * </p>
121 *
122 * @param charSequence The character sequence, may be {@code null}
123 * @param start The start index in the character sequence, inclusive
124 * @param end The end index in the character sequence, exclusive
125 * @throws IllegalArgumentException if the start index is negative, or if the end index is smaller than the start index
126 * @since 2.7
127 */
128 public CharSequenceReader(final CharSequence charSequence, final int start, final int end) {
129 if (start < 0) {
130 throw new IllegalArgumentException("Start index is less than zero: " + start);
131 }
132 if (end < start) {
133 throw new IllegalArgumentException("End index is less than start " + start + ": " + end);
134 }
135 // Don't check the start and end indexes against the CharSequence,
136 // to let it grow and shrink without breaking existing behavior.
137
138 this.charSequence = charSequence != null ? charSequence : "";
139 this.start = start;
140 this.end = end;
141
142 this.idx = start;
143 this.mark = start;
144 }
145
146 /**
147 * Close resets the file back to the start and removes any marked position.
148 */
149 @Override
150 public void close() {
151 idx = start;
152 mark = start;
153 }
154
155 /**
156 * Returns the index in the character sequence to end reading at, taking into account its length.
157 *
158 * @return The end index in the character sequence (exclusive).
159 */
160 private int end() {
161 /*
162 * end == null for de-serialized instances that were serialized before start and end were added.
163 * Use Integer.MAX_VALUE to get the same behavior as before - use the entire CharSequence.
164 */
165 return Math.min(charSequence.length(), end == null ? Integer.MAX_VALUE : end);
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 * Reads 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 * Reads 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 no more
209 * @throws NullPointerException if the array is {@code null}.
210 * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code array.length}.
211 */
212 @Override
213 public int read(final char[] array, final int offset, final int length) {
214 IOUtils.checkFromIndexSize(array, offset, length);
215 if (length == 0) {
216 return 0;
217 }
218 if (idx >= end()) {
219 return EOF;
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 * Tells whether this stream is ready to be read.
255 *
256 * @return {@code true} if more characters from the character sequence are available, or {@code false} otherwise.
257 */
258 @Override
259 public boolean ready() {
260 return idx < end();
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("Number of characters to skip is less than zero: " + n);
282 }
283 if (idx >= end()) {
284 return 0;
285 }
286 final int dest = (int) Math.min(end(), idx + n);
287 final int count = dest - idx;
288 idx = dest;
289 return count;
290 }
291
292 /**
293 * Returns the index in the character sequence to start reading from, taking into account its length.
294 *
295 * @return The start index in the character sequence (inclusive).
296 */
297 private int start() {
298 return Math.min(charSequence.length(), start);
299 }
300
301 /**
302 * Gets a String representation of the underlying
303 * character sequence.
304 *
305 * @return The contents of the character sequence
306 */
307 @Override
308 public String toString() {
309 return charSequence.subSequence(start(), end()).toString();
310 }
311 }