View Javadoc
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  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      * Read 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      * Read 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 }