Skip to content

Commit f7e0f2c

Browse files
committed
Let subclasses detect when reading past the maximum is requested
Javadoc
1 parent 1010f48 commit f7e0f2c

2 files changed

Lines changed: 128 additions & 29 deletions

File tree

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

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,11 @@
2222
import java.io.InputStream;
2323

2424
/**
25-
* This is a stream that will only supply bytes up to a certain length - if its
26-
* position goes above that, it will stop.
25+
* Reads bytes up to a maximum length, if its count goes above that, it stops.
2726
* <p>
28-
* This is useful to wrap ServletInputStreams. The ServletInputStream will block
29-
* if you try to read content from it that isn't there, because it doesn't know
30-
* whether the content hasn't arrived yet or whether the content has finished.
31-
* So, one of these, initialized with the Content-length sent in the
32-
* ServletInputStream's header, will stop it blocking, providing it's been sent
33-
* with a correct content length.
27+
* This is useful to wrap ServletInputStreams. The ServletInputStream will block if you try to read content from it that isn't there, because it doesn't know
28+
* whether the content hasn't arrived yet or whether the content has finished. So, one of these, initialized with the Content-length sent in the
29+
* ServletInputStream's header, will stop it blocking, providing it's been sent with a correct content length.
3430
* </p>
3531
*
3632
* @since 2.0
@@ -40,11 +36,11 @@ public class BoundedInputStream extends InputStream {
4036
/** The wrapped input stream. */
4137
private final InputStream inputStream;
4238

43-
/** The max length to read. */
44-
private final long maxLength;
39+
/** The max count of bytes to read. */
40+
private final long maxCount;
4541

46-
/** The number of bytes already returned. */
47-
private long pos;
42+
/** The count of bytes read. */
43+
private long count;
4844

4945
/** The marked position. */
5046
private long mark = EOF;
@@ -53,26 +49,26 @@ public class BoundedInputStream extends InputStream {
5349
private boolean propagateClose = true;
5450

5551
/**
56-
* Creates a new {@link BoundedInputStream} that wraps the given input
52+
* Constructs a new {@link BoundedInputStream} that wraps the given input
5753
* stream and is unlimited.
5854
*
59-
* @param in The wrapped input stream
55+
* @param in The wrapped input stream.
6056
*/
6157
public BoundedInputStream(final InputStream in) {
6258
this(in, EOF);
6359
}
6460

6561
/**
66-
* Creates a new {@link BoundedInputStream} that wraps the given input
62+
* Constructs a new {@link BoundedInputStream} that wraps the given input
6763
* stream and limits it to a certain size.
6864
*
69-
* @param inputStream The wrapped input stream
70-
* @param maxLength The maximum number of bytes to return
65+
* @param inputStream The wrapped input stream.
66+
* @param maxLength The maximum number of bytes to return.
7167
*/
7268
public BoundedInputStream(final InputStream inputStream, final long maxLength) {
7369
// Some badly designed methods - e.g. the servlet API - overload length
7470
// such that "-1" means stream finished
75-
this.maxLength = maxLength;
71+
this.maxCount = maxLength;
7672
this.inputStream = inputStream;
7773
}
7874

@@ -81,7 +77,8 @@ public BoundedInputStream(final InputStream inputStream, final long maxLength) {
8177
*/
8278
@Override
8379
public int available() throws IOException {
84-
if (maxLength >= 0 && pos >= maxLength) {
80+
if (isMaxLength()) {
81+
onMaxLength(maxCount, count);
8582
return 0;
8683
}
8784
return inputStream.available();
@@ -90,6 +87,7 @@ public int available() throws IOException {
9087
/**
9188
* Invokes the delegate's {@code close()} method
9289
* if {@link #isPropagateClose()} is {@code true}.
90+
*
9391
* @throws IOException if an I/O error occurs.
9492
*/
9593
@Override
@@ -100,7 +98,31 @@ public void close() throws IOException {
10098
}
10199

102100
/**
103-
* Indicates whether the {@link #close()} method
101+
* Gets the count of bytes read.
102+
*
103+
* @return The count of bytes read.
104+
* @since 2.12.0
105+
*/
106+
public long getCount() {
107+
return count;
108+
}
109+
110+
/**
111+
* Gets the max count of bytes to read.
112+
*
113+
* @return The max count of bytes to read.
114+
* @since 2.12.0
115+
*/
116+
public long getMaxLength() {
117+
return maxCount;
118+
}
119+
120+
private boolean isMaxLength() {
121+
return maxCount >= 0 && count >= maxCount;
122+
}
123+
124+
/**
125+
* Tests whether the {@link #close()} method
104126
* should propagate to the underling {@link InputStream}.
105127
*
106128
* @return {@code true} if calling {@link #close()}
@@ -113,42 +135,59 @@ public boolean isPropagateClose() {
113135

114136
/**
115137
* Invokes the delegate's {@code mark(int)} method.
138+
*
116139
* @param readlimit read ahead limit
117140
*/
118141
@Override
119142
public synchronized void mark(final int readlimit) {
120143
inputStream.mark(readlimit);
121-
mark = pos;
144+
mark = count;
122145
}
123146

124147
/**
125148
* Invokes the delegate's {@code markSupported()} method.
149+
*
126150
* @return true if mark is supported, otherwise false
127151
*/
128152
@Override
129153
public boolean markSupported() {
130154
return inputStream.markSupported();
131155
}
132156

157+
/**
158+
* A caller has caused a request that would cross the {@code maxLength} boundary.
159+
*
160+
* @param maxLength The max count of bytes to read.
161+
* @param count The count of bytes read.
162+
* @throws IOException Subclasses may throw.
163+
* @since 2.12.0
164+
*/
165+
protected void onMaxLength(final long maxLength, final long count) throws IOException {
166+
// for subclasses
167+
}
168+
133169
/**
134170
* Invokes the delegate's {@code read()} method if
135171
* the current position is less than the limit.
172+
*
136173
* @return the byte read or -1 if the end of stream or
137174
* the limit has been reached.
138175
* @throws IOException if an I/O error occurs.
139176
*/
140177
@Override
141178
public int read() throws IOException {
142-
if (maxLength >= 0 && pos >= maxLength) {
179+
if (isMaxLength()) {
180+
onMaxLength(maxCount, count);
143181
return EOF;
144182
}
145183
final int result = inputStream.read();
146-
pos++;
184+
count++;
147185
return result;
148186
}
149187

150188
/**
151189
* Invokes the delegate's {@code read(byte[])} method.
190+
*
152191
* @param b the buffer to read the bytes into
153192
* @return the number of bytes read or -1 if the end of stream or
154193
* the limit has been reached.
@@ -161,6 +200,7 @@ public int read(final byte[] b) throws IOException {
161200

162201
/**
163202
* Invokes the delegate's {@code read(byte[], int, int)} method.
203+
*
164204
* @param b the buffer to read the bytes into
165205
* @param off The start offset
166206
* @param len The number of bytes to read
@@ -170,28 +210,30 @@ public int read(final byte[] b) throws IOException {
170210
*/
171211
@Override
172212
public int read(final byte[] b, final int off, final int len) throws IOException {
173-
if (maxLength >= 0 && pos >= maxLength) {
213+
if (isMaxLength()) {
214+
onMaxLength(maxCount, count);
174215
return EOF;
175216
}
176-
final long maxRead = maxLength >= 0 ? Math.min(len, maxLength - pos) : len;
217+
final long maxRead = maxCount >= 0 ? Math.min(len, maxCount - count) : len;
177218
final int bytesRead = inputStream.read(b, off, (int) maxRead);
178219

179220
if (bytesRead == EOF) {
180221
return EOF;
181222
}
182223

183-
pos += bytesRead;
224+
count += bytesRead;
184225
return bytesRead;
185226
}
186227

187228
/**
188229
* Invokes the delegate's {@code reset()} method.
230+
*
189231
* @throws IOException if an I/O error occurs.
190232
*/
191233
@Override
192234
public synchronized void reset() throws IOException {
193235
inputStream.reset();
194-
pos = mark;
236+
count = mark;
195237
}
196238

197239
/**
@@ -209,20 +251,22 @@ public void setPropagateClose(final boolean propagateClose) {
209251

210252
/**
211253
* Invokes the delegate's {@code skip(long)} method.
254+
*
212255
* @param n the number of bytes to skip
213256
* @return the actual number of bytes skipped
214257
* @throws IOException if an I/O error occurs.
215258
*/
216259
@Override
217260
public long skip(final long n) throws IOException {
218-
final long toSkip = maxLength >= 0 ? Math.min(n, maxLength - pos) : n;
261+
final long toSkip = maxCount >= 0 ? Math.min(n, maxCount - count) : n;
219262
final long skippedBytes = inputStream.skip(toSkip);
220-
pos += skippedBytes;
263+
count += skippedBytes;
221264
return skippedBytes;
222265
}
223266

224267
/**
225268
* Invokes the delegate's {@code toString()} method.
269+
*
226270
* @return the delegate's {@code toString()}
227271
*/
228272
@Override

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
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.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
2022

2123
import java.io.ByteArrayInputStream;
24+
import java.util.concurrent.atomic.AtomicBoolean;
2225

2326
import org.apache.commons.io.IOUtils;
2427
import org.junit.jupiter.api.Test;
@@ -35,6 +38,58 @@ private void compare(final String msg, final byte[] expected, final byte[] actua
3538
}
3639
}
3740

41+
@Test
42+
public void testOnMaxLength() throws Exception {
43+
BoundedInputStream bounded;
44+
final byte[] helloWorld = "Hello World".getBytes();
45+
final byte[] hello = "Hello".getBytes();
46+
final AtomicBoolean boolRef = new AtomicBoolean();
47+
48+
// limit = length
49+
bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length) {
50+
@Override
51+
protected void onMaxLength(long max, long readCount) {
52+
boolRef.set(true);
53+
}
54+
};
55+
assertFalse(boolRef.get());
56+
for (int i = 0; i < helloWorld.length; i++) {
57+
assertEquals(helloWorld[i], bounded.read(), "limit = length byte[" + i + "]");
58+
}
59+
assertEquals(-1, bounded.read(), "limit = length end");
60+
assertTrue(boolRef.get());
61+
62+
// limit > length
63+
boolRef.set(false);
64+
bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length + 1) {
65+
@Override
66+
protected void onMaxLength(long max, long readCount) {
67+
boolRef.set(true);
68+
}
69+
};
70+
assertFalse(boolRef.get());
71+
for (int i = 0; i < helloWorld.length; i++) {
72+
assertEquals(helloWorld[i], bounded.read(), "limit > length byte[" + i + "]");
73+
}
74+
assertEquals(-1, bounded.read(), "limit > length end");
75+
assertFalse(boolRef.get());
76+
77+
// limit < length
78+
boolRef.set(false);
79+
bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), hello.length) {
80+
@Override
81+
protected void onMaxLength(long max, long readCount) {
82+
boolRef.set(true);
83+
}
84+
};
85+
assertFalse(boolRef.get());
86+
for (int i = 0; i < hello.length; i++) {
87+
assertEquals(hello[i], bounded.read(), "limit < length byte[" + i + "]");
88+
}
89+
assertEquals(-1, bounded.read(), "limit < length end");
90+
assertTrue(boolRef.get());
91+
}
92+
3893
@Test
3994
public void testReadArray() throws Exception {
4095

0 commit comments

Comments
 (0)