Skip to content

Commit 39e3ea7

Browse files
authored
Support sub sequences in CharSequenceReader (#91)
* Added support for sub sequences in CharSequenceReader * Added missing Javadoc. Added entry to changes.xml. * Added missing @SInCE tags. * [IO-619] Fixed issues reported in merge request #91 * [IO-619] Use assertThrows instead of try-fail-catch * [IO-619] Fixed issues reported in merge request #91
1 parent 101f3c2 commit 39e3ea7

4 files changed

Lines changed: 247 additions & 12 deletions

File tree

src/changes/changes.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,12 @@ The <action> type attribute can be add,update,fix,remove.
128128
<action issue="IO-617" dev="ggregory" type="add" due-to="Rob Spoor, Gary Gregory">
129129
Add class CloseShieldWriter. #83.
130130
</action>
131-
<action issue="IO-617" dev="ggregory" type="add" due-to="Rob Spoor">
131+
<action issue="IO-618" dev="ggregory" type="add" due-to="Rob Spoor">
132132
Add classes Added TaggedReader, ClosedReader and BrokenReader. #85.
133133
</action>
134+
<action issue="IO-619" dev="ggregory" type="add" due-to="Rob Spoor">
135+
Support sub sequences in CharSequenceReader. #91.
136+
</action>
134137
<action issue="IO-625" dev="ggregory" type="fix" due-to="Mikko Maunu">
135138
Corrected misleading exception message for FileUtils.copyDirectoryToDirectory.
136139
</action>

src/main/java/org/apache/commons/io/input/CharSequenceReader.java

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* StringBuilder or CharBuffer.
2828
* <p>
2929
* <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
30+
* </p>
3031
*
3132
* @since 1.4
3233
*/
@@ -38,21 +39,130 @@ public class CharSequenceReader extends Reader implements Serializable {
3839
private int mark;
3940

4041
/**
41-
* Construct a new instance with the specified character sequence.
42+
* The start index in the character sequence, inclusive.
43+
* <p>
44+
* When de-serializing a CharSequenceReader that was serialized before
45+
* this fields was added, this field will be initialized to 0, which
46+
* gives the same behavior as before: start reading from the start.
47+
* </p>
48+
*
49+
* @see #start()
50+
* @since 2.7
51+
*/
52+
private final int start;
53+
54+
/**
55+
* The end index in the character sequence, exclusive.
56+
* <p>
57+
* When de-serializing a CharSequenceReader that was serialized before
58+
* this fields was added, this field will be initialized to {@code null},
59+
* which gives the same behavior as before: stop reading at the
60+
* CharSequence's length.
61+
* If this field was an int instead, it would be initialized to 0 when the
62+
* CharSequenceReader is de-serialized, causing it to not return any
63+
* characters at all.
64+
* </p>
65+
*
66+
* @see #end()
67+
* @since 2.7
68+
*/
69+
private final Integer end;
70+
71+
/**
72+
* Constructs a new instance with the specified character sequence.
4273
*
4374
* @param charSequence The character sequence, may be {@code null}
4475
*/
4576
public CharSequenceReader(final CharSequence charSequence) {
77+
this(charSequence, 0);
78+
}
79+
80+
/**
81+
* Constructs a new instance with a portion of the specified character sequence.
82+
* <p>
83+
* The start index is not strictly enforced to be within the bounds of the
84+
* character sequence. This allows the character sequence to grow or shrink
85+
* in size without risking any {@link IndexOutOfBoundsException} to be thrown.
86+
* Instead, if the character sequence grows smaller than the start index, this
87+
* instance will act as if all characters have been read.
88+
* </p>
89+
*
90+
* @param charSequence The character sequence, may be {@code null}
91+
* @param start The start index in the character sequence, inclusive
92+
* @throws IllegalArgumentException if the start index is negative
93+
* @since 2.7
94+
*/
95+
public CharSequenceReader(final CharSequence charSequence, final int start) {
96+
this(charSequence, start, Integer.MAX_VALUE);
97+
}
98+
99+
/**
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+
46129
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);
47157
}
48158

49159
/**
50160
* Close resets the file back to the start and removes any marked position.
51161
*/
52162
@Override
53163
public void close() {
54-
idx = 0;
55-
mark = 0;
164+
idx = start;
165+
mark = start;
56166
}
57167

58168
/**
@@ -83,7 +193,7 @@ public boolean markSupported() {
83193
*/
84194
@Override
85195
public int read() {
86-
if (idx >= charSequence.length()) {
196+
if (idx >= end()) {
87197
return EOF;
88198
}
89199
return charSequence.charAt(idx++);
@@ -100,7 +210,7 @@ public int read() {
100210
*/
101211
@Override
102212
public int read(final char[] array, final int offset, final int length) {
103-
if (idx >= charSequence.length()) {
213+
if (idx >= end()) {
104214
return EOF;
105215
}
106216
Objects.requireNonNull(array, "array");
@@ -110,19 +220,19 @@ public int read(final char[] array, final int offset, final int length) {
110220
}
111221

112222
if (charSequence instanceof String) {
113-
final int count = Math.min(length, charSequence.length() - idx);
223+
final int count = Math.min(length, end() - idx);
114224
((String) charSequence).getChars(idx, idx + count, array, offset);
115225
idx += count;
116226
return count;
117227
}
118228
if (charSequence instanceof StringBuilder) {
119-
final int count = Math.min(length, charSequence.length() - idx);
229+
final int count = Math.min(length, end() - idx);
120230
((StringBuilder) charSequence).getChars(idx, idx + count, array, offset);
121231
idx += count;
122232
return count;
123233
}
124234
if (charSequence instanceof StringBuffer) {
125-
final int count = Math.min(length, charSequence.length() - idx);
235+
final int count = Math.min(length, end() - idx);
126236
((StringBuffer) charSequence).getChars(idx, idx + count, array, offset);
127237
idx += count;
128238
return count;
@@ -161,10 +271,10 @@ public long skip(final long n) {
161271
throw new IllegalArgumentException(
162272
"Number of characters to skip is less than zero: " + n);
163273
}
164-
if (idx >= charSequence.length()) {
274+
if (idx >= end()) {
165275
return EOF;
166276
}
167-
final int dest = (int)Math.min(charSequence.length(), idx + n);
277+
final int dest = (int)Math.min(end(), idx + n);
168278
final int count = dest - idx;
169279
idx = dest;
170280
return count;
@@ -178,6 +288,7 @@ public long skip(final long n) {
178288
*/
179289
@Override
180290
public String toString() {
181-
return charSequence.toString();
291+
CharSequence subSequence = charSequence.subSequence(start(), end());
292+
return subSequence.toString();
182293
}
183294
}

src/test/java/org/apache/commons/io/input/CharSequenceReaderTest.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717
package org.apache.commons.io.input;
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
2021
import static org.junit.jupiter.api.Assertions.assertTrue;
2122

23+
import java.io.ByteArrayInputStream;
24+
import java.io.ByteArrayOutputStream;
2225
import java.io.IOException;
26+
import java.io.ObjectInputStream;
27+
import java.io.ObjectOutputStream;
2328
import java.io.Reader;
2429
import java.nio.CharBuffer;
30+
import java.util.Arrays;
2531

2632
import org.junit.jupiter.api.Test;
2733

@@ -38,6 +44,11 @@ public void testClose() throws IOException {
3844
checkRead(reader, "Foo");
3945
reader.close();
4046
checkRead(reader, "Foo");
47+
48+
final Reader subReader = new CharSequenceReader("xFooBarx", 1, 7);
49+
checkRead(subReader, "Foo");
50+
subReader.close();
51+
checkRead(subReader, "Foo");
4152
}
4253

4354
@Test
@@ -60,6 +71,17 @@ public void testMark() throws IOException {
6071
reader.reset();
6172
checkRead(reader, "Foo");
6273
}
74+
try (final Reader subReader = new CharSequenceReader("xFooBarx", 1, 7)) {
75+
checkRead(subReader, "Foo");
76+
subReader.mark(0);
77+
checkRead(subReader, "Bar");
78+
subReader.reset();
79+
checkRead(subReader, "Bar");
80+
subReader.close();
81+
checkRead(subReader, "Foo");
82+
subReader.reset();
83+
checkRead(subReader, "Foo");
84+
}
6385
}
6486

6587
@Test
@@ -75,6 +97,18 @@ public void testSkip() throws IOException {
7597
reader.close();
7698
assertEquals(6, reader.skip(20));
7799
assertEquals(-1, reader.read());
100+
101+
final Reader subReader = new CharSequenceReader("xFooBarx", 1, 7);
102+
assertEquals(3, subReader.skip(3));
103+
checkRead(subReader, "Bar");
104+
assertEquals(-1, subReader.skip(3));
105+
subReader.reset();
106+
assertEquals(2, subReader.skip(2));
107+
assertEquals(4, subReader.skip(10));
108+
assertEquals(-1, subReader.skip(1));
109+
subReader.close();
110+
assertEquals(6, subReader.skip(20));
111+
assertEquals(-1, subReader.read());
78112
}
79113

80114
@Test
@@ -94,6 +128,12 @@ private void testRead(final CharSequence charSequence) throws IOException {
94128
assertEquals(-1, reader.read());
95129
assertEquals(-1, reader.read());
96130
}
131+
try (final Reader reader = new CharSequenceReader(charSequence, 1, 5)) {
132+
assertEquals('o', reader.read());
133+
assertEquals('o', reader.read());
134+
assertEquals(-1, reader.read());
135+
assertEquals(-1, reader.read());
136+
}
97137
}
98138

99139
@Test
@@ -118,6 +158,18 @@ private void testReadCharArray(final CharSequence charSequence) throws IOExcepti
118158
checkArray(new char[] { 'r', NONE, NONE }, chars);
119159
assertEquals(-1, reader.read(chars));
120160
}
161+
try (final Reader reader = new CharSequenceReader(charSequence, 1, 5)) {
162+
char[] chars = new char[2];
163+
assertEquals(2, reader.read(chars));
164+
checkArray(new char[] { 'o', 'o' }, chars);
165+
chars = new char[3];
166+
assertEquals(2, reader.read(chars));
167+
checkArray(new char[] { 'B', 'a', NONE }, chars);
168+
chars = new char[3];
169+
assertEquals(-1, reader.read(chars));
170+
checkArray(new char[] { NONE, NONE, NONE }, chars);
171+
assertEquals(-1, reader.read(chars));
172+
}
121173
}
122174

123175
@Test
@@ -138,6 +190,14 @@ private void testReadCharArrayPortion(final CharSequence charSequence) throws IO
138190
checkArray(new char[] { 'B', 'a', 'r', 'F', 'o', 'o', NONE }, chars);
139191
assertEquals(-1, reader.read(chars));
140192
}
193+
Arrays.fill(chars, NONE);
194+
try (final Reader reader = new CharSequenceReader(charSequence, 1, 5)) {
195+
assertEquals(2, reader.read(chars, 3, 2));
196+
checkArray(new char[] { NONE, NONE, NONE, 'o', 'o', NONE }, chars);
197+
assertEquals(2, reader.read(chars, 0, 3));
198+
checkArray(new char[] { 'B', 'a', NONE, 'o', 'o', NONE }, chars);
199+
assertEquals(-1, reader.read(chars));
200+
}
141201
}
142202

143203
private void checkRead(final Reader reader, final String expected) throws IOException {
@@ -151,4 +211,65 @@ private void checkArray(final char[] expected, final char[] actual) {
151211
assertEquals(expected[i], actual[i], "Compare[" +i + "]");
152212
}
153213
}
214+
215+
@Test
216+
public void testConstructor() {
217+
assertThrows(IllegalArgumentException.class, () -> new CharSequenceReader("FooBar", -1, 6),
218+
"Expected exception not thrown for negative start.");
219+
assertThrows(IllegalArgumentException.class, () -> new CharSequenceReader("FooBar", 1, 0),
220+
"Expected exception not thrown for end before start.");
221+
}
222+
223+
@Test
224+
public void testToString() {
225+
assertEquals("FooBar", new CharSequenceReader("FooBar").toString());
226+
assertEquals("FooBar", new CharSequenceReader("xFooBarx", 1, 7).toString());
227+
}
228+
229+
@Test
230+
public void testSerialization() throws IOException, ClassNotFoundException {
231+
/*
232+
* File CharSequenceReader.bin contains a CharSequenceReader that was serialized before
233+
* the start and end fields were added. Its CharSequence is "FooBar".
234+
* This part of the test will test that adding the fields does not break any existing
235+
* serialized CharSequenceReaders.
236+
*/
237+
try (ObjectInputStream ois = new ObjectInputStream(getClass().getResourceAsStream("CharSequenceReader.bin"))) {
238+
final CharSequenceReader reader = (CharSequenceReader) ois.readObject();
239+
assertEquals('F', reader.read());
240+
assertEquals('o', reader.read());
241+
assertEquals('o', reader.read());
242+
assertEquals('B', reader.read());
243+
assertEquals('a', reader.read());
244+
assertEquals('r', reader.read());
245+
assertEquals(-1, reader.read());
246+
assertEquals(-1, reader.read());
247+
}
248+
249+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
250+
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
251+
final CharSequenceReader reader = new CharSequenceReader("xFooBarx", 1, 7);
252+
oos.writeObject(reader);
253+
}
254+
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
255+
final CharSequenceReader reader = (CharSequenceReader) ois.readObject();
256+
assertEquals('F', reader.read());
257+
assertEquals('o', reader.read());
258+
assertEquals('o', reader.read());
259+
assertEquals('B', reader.read());
260+
assertEquals('a', reader.read());
261+
assertEquals('r', reader.read());
262+
assertEquals(-1, reader.read());
263+
assertEquals(-1, reader.read());
264+
reader.reset();
265+
assertEquals('F', reader.read());
266+
assertEquals('o', reader.read());
267+
assertEquals('o', reader.read());
268+
assertEquals('B', reader.read());
269+
assertEquals('a', reader.read());
270+
assertEquals('r', reader.read());
271+
assertEquals(-1, reader.read());
272+
assertEquals(-1, reader.read());
273+
}
274+
}
154275
}
Binary file not shown.

0 commit comments

Comments
 (0)