Skip to content

Commit

Permalink
Add BrokenInputStream.Builder
Browse files Browse the repository at this point in the history
  • Loading branch information
garydgregory committed Dec 23, 2023
1 parent 7d41cb1 commit a01bbfb
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 47 deletions.
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ The <action> type attribute can be add,update,fix,remove.
<action dev="ggregory" type="add" due-to="Gary Gregory">Add FileTimes.isUnixTime(FileTime).</action>
<action dev="ggregory" type="add" due-to="Gary Gregory">Add FileTimes.isUnixTime(long).</action>
<action dev="ggregory" type="add" due-to="Gary Gregory">Add FileTimes.toUnixTime(FileTime).</action>
<action dev="ggregory" type="add" due-to="Gary Gregory">Add BrokenInputStream.Builder.</action>
<!-- UPDATE -->
<action dev="ggregory" type="fix" due-to="Gary Gregory">Bump commons.bytebuddy.version from 1.14.10 to 1.14.11 #534.</action>
</release>
Expand Down
148 changes: 119 additions & 29 deletions src/main/java/org/apache/commons/io/input/BoundedInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.build.AbstractStreamBuilder;

/**
* Reads bytes up to a maximum length, if its count goes above that, it stops.
* <p>
Expand All @@ -34,6 +37,80 @@
*/
public class BoundedInputStream extends FilterInputStream {

/**
* Builds a new {@link BoundedInputStream} instance.
*
* <h2>Using NIO</h2>
*
* <pre>{@code
* BoundedInputStream s = BoundedInputStream.builder().setPath(Paths.get("MyFile.xml")).setMaxLength(1024).setPropagateClose(false).get();
* }
* </pre>
*
* <h2>Using IO</h2>
*
* <pre>{@code
* BoundedInputStream s = BoundedInputStream.builder().setFile(new File("MyFile.xml")).setMaxLength(1024).setPropagateClose(false).get();
* }
* </pre>
*
* @since 2.16.0
*/
public static class Builder extends AbstractStreamBuilder<BoundedInputStream, Builder> {

/** The max count of bytes to read. */
private long maxLength = EOF;

/** Flag if close should be propagated. */
private boolean propagateClose = true;

@SuppressWarnings("resource")
@Override
public BoundedInputStream get() throws IOException {
return new BoundedInputStream(getInputStream(), maxLength, propagateClose);
}

/**
* Sets the maximum number of bytes to return.
* <p>
* Default is {@value IOUtils#EOF}.
* </p>
*
* @param maxLength The maximum number of bytes to return.
* @return this.
*/
public Builder setMaxLength(final long maxLength) {
this.maxLength = maxLength;
return this;
}

/**
* Sets whether the {@link #close()} method should propagate to the underling {@link InputStream}.
* <p>
* Default is true.
* </p>
*
* @param propagateClose {@code true} if calling {@link #close()} propagates to the {@code close()} method of the underlying stream or {@code false} if
* it does not.
* @return this.
*/
public Builder setPropagateClose(final boolean propagateClose) {
this.propagateClose = propagateClose;
return this;
}

}

/**
* Constructs a new {@link Builder}.
*
* @return a new {@link Builder}.
* @since 2.16.0
*/
public static Builder builder() {
return new Builder();
}

/** The max count of bytes to read. */
private final long maxCount;

Expand All @@ -43,33 +120,55 @@ public class BoundedInputStream extends FilterInputStream {
/** The marked position. */
private long mark = EOF;

/** Flag if close should be propagated. */
/**
* Flag if close should be propagated.
*
* TODO Make final in 3.0.
*/
private boolean propagateClose = true;

/**
* Constructs a new {@link BoundedInputStream} that wraps the given input
* stream and is unlimited.
* Constructs a new {@link BoundedInputStream} that wraps the given input stream and is unlimited.
*
* @param in The wrapped input stream.
* @deprecated Use {@link Builder#get()}.
*/
@Deprecated
public BoundedInputStream(final InputStream in) {
this(in, EOF);
}

/**
* Constructs a new {@link BoundedInputStream} that wraps the given input
* stream and limits it to a certain size.
* Constructs a new {@link BoundedInputStream} that wraps the given input stream and limits it to a certain size.
*
* @param inputStream The wrapped input stream.
* @param maxLength The maximum number of bytes to return.
* @param maxLength The maximum number of bytes to return.
* @deprecated Use {@link Builder#get()}.
*/
@Deprecated
public BoundedInputStream(final InputStream inputStream, final long maxLength) {
// Some badly designed methods - e.g. the servlet API - overload length
// such that "-1" means stream finished
super(inputStream);
this.maxCount = maxLength;
}

/**
* Constructs a new {@link BoundedInputStream} that wraps the given input stream and limits it to a certain size.
*
* @param inputStream The wrapped input stream.
* @param maxLength The maximum number of bytes to return.
* @param propagateClose {@code true} if calling {@link #close()} propagates to the {@code close()} method of the underlying stream or {@code false} if it
* does not.
*/
private BoundedInputStream(final InputStream inputStream, final long maxLength, final boolean propagateClose) {
// Some badly designed methods - e.g. the servlet API - overload length
// such that "-1" means stream finished
super(inputStream);
this.maxCount = maxLength;
this.propagateClose = propagateClose;
}

/**
* {@inheritDoc}
*/
Expand All @@ -83,8 +182,7 @@ public int available() throws IOException {
}

/**
* Invokes the delegate's {@code close()} method
* if {@link #isPropagateClose()} is {@code true}.
* Invokes the delegate's {@code close()} method if {@link #isPropagateClose()} is {@code true}.
*
* @throws IOException if an I/O error occurs.
*/
Expand Down Expand Up @@ -130,12 +228,9 @@ private boolean isMaxLength() {
}

/**
* Tests whether the {@link #close()} method
* should propagate to the underling {@link InputStream}.
* Tests whether the {@link #close()} method should propagate to the underling {@link InputStream}.
*
* @return {@code true} if calling {@link #close()}
* propagates to the {@code close()} method of the
* underlying stream or {@code false} if it does not.
* @return {@code true} if calling {@link #close()} propagates to the {@code close()} method of the underlying stream or {@code false} if it does not.
*/
public boolean isPropagateClose() {
return propagateClose;
Expand Down Expand Up @@ -166,7 +261,7 @@ public boolean markSupported() {
* A caller has caused a request that would cross the {@code maxLength} boundary.
*
* @param maxLength The max count of bytes to read.
* @param count The count of bytes read.
* @param count The count of bytes read.
* @throws IOException Subclasses may throw.
* @since 2.12.0
*/
Expand All @@ -176,11 +271,9 @@ protected void onMaxLength(final long maxLength, final long count) throws IOExce
}

/**
* Invokes the delegate's {@code read()} method if
* the current position is less than the limit.
* Invokes the delegate's {@code read()} method if the current position is less than the limit.
*
* @return the byte read or -1 if the end of stream or
* the limit has been reached.
* @return the byte read or -1 if the end of stream or the limit has been reached.
* @throws IOException if an I/O error occurs.
*/
@Override
Expand All @@ -200,8 +293,7 @@ public int read() throws IOException {
* Invokes the delegate's {@code read(byte[])} method.
*
* @param b the buffer to read the bytes into
* @return the number of bytes read or -1 if the end of stream or
* the limit has been reached.
* @return the number of bytes read or -1 if the end of stream or the limit has been reached.
* @throws IOException if an I/O error occurs.
*/
@Override
Expand All @@ -212,11 +304,10 @@ public int read(final byte[] b) throws IOException {
/**
* Invokes the delegate's {@code read(byte[], int, int)} method.
*
* @param b the buffer to read the bytes into
* @param b the buffer to read the bytes into
* @param off The start offset
* @param len The number of bytes to read
* @return the number of bytes read or -1 if the end of stream or
* the limit has been reached.
* @return the number of bytes read or -1 if the end of stream or the limit has been reached.
* @throws IOException if an I/O error occurs.
*/
@Override
Expand Down Expand Up @@ -248,14 +339,13 @@ public synchronized void reset() throws IOException {
}

/**
* Sets whether the {@link #close()} method
* should propagate to the underling {@link InputStream}.
* Sets whether the {@link #close()} method should propagate to the underling {@link InputStream}.
*
* @param propagateClose {@code true} if calling
* {@link #close()} propagates to the {@code close()}
* method of the underlying stream or
* {@code false} if it does not.
* @param propagateClose {@code true} if calling {@link #close()} propagates to the {@code close()} method of the underlying stream or {@code false} if it
* does not.
* @deprecated Use {@link Builder#setPropagateClose(boolean)}.
*/
@Deprecated
public void setPropagateClose(final boolean propagateClose) {
this.propagateClose = propagateClose;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.io.IOUtils;
Expand All @@ -38,11 +39,12 @@ private void compare(final String msg, final byte[] expected, final byte[] actua
}
}

@SuppressWarnings("deprecation")
@Test
public void testOnMaxLength() throws Exception {
BoundedInputStream bounded;
final byte[] helloWorld = "Hello World".getBytes();
final byte[] hello = "Hello".getBytes();
final byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8);
final byte[] hello = "Hello".getBytes(StandardCharsets.UTF_8);
final AtomicBoolean boolRef = new AtomicBoolean();

// limit = length
Expand Down Expand Up @@ -127,31 +129,38 @@ protected void onMaxLength(final long max, final long readCount) {
@Test
public void testReadArray() throws Exception {

BoundedInputStream bounded;
final byte[] helloWorld = "Hello World".getBytes();
final byte[] hello = "Hello".getBytes();

bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld));
compare("limit = -1", helloWorld, IOUtils.toByteArray(bounded));
final byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8);
final byte[] hello = "Hello".getBytes(StandardCharsets.UTF_8);

bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), 0);
compare("limit = 0", IOUtils.EMPTY_BYTE_ARRAY, IOUtils.toByteArray(bounded));
try (BoundedInputStream bounded = BoundedInputStream.builder().setInputStream(new ByteArrayInputStream(helloWorld)).get()) {
compare("limit = -1", helloWorld, IOUtils.toByteArray(bounded));
}

bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length);
compare("limit = length", helloWorld, IOUtils.toByteArray(bounded));
try (BoundedInputStream bounded = BoundedInputStream.builder().setInputStream(new ByteArrayInputStream(helloWorld)).setMaxLength(0).get()) {
compare("limit = 0", IOUtils.EMPTY_BYTE_ARRAY, IOUtils.toByteArray(bounded));
}

bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length + 1);
compare("limit > length", helloWorld, IOUtils.toByteArray(bounded));
try (BoundedInputStream bounded = BoundedInputStream.builder().setInputStream(new ByteArrayInputStream(helloWorld)).setMaxLength(helloWorld.length)
.get()) {
compare("limit = length", helloWorld, IOUtils.toByteArray(bounded));
}

bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length - 6);
compare("limit < length", hello, IOUtils.toByteArray(bounded));
try (BoundedInputStream bounded = BoundedInputStream.builder().setInputStream(new ByteArrayInputStream(helloWorld)).setMaxLength(helloWorld.length + 1)
.get()) {
compare("limit > length", helloWorld, IOUtils.toByteArray(bounded));
}
try (BoundedInputStream bounded = BoundedInputStream.builder().setInputStream(new ByteArrayInputStream(helloWorld)).setMaxLength(helloWorld.length - 6)
.get()) {
compare("limit < length", hello, IOUtils.toByteArray(bounded));
}
}

@SuppressWarnings("deprecation")
@Test
public void testReadSingle() throws Exception {
BoundedInputStream bounded;
final byte[] helloWorld = "Hello World".getBytes();
final byte[] hello = "Hello".getBytes();
final byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8);
final byte[] hello = "Hello".getBytes(StandardCharsets.UTF_8);

// limit = length
bounded = new BoundedInputStream(new ByteArrayInputStream(helloWorld), helloWorld.length);
Expand Down

0 comments on commit a01bbfb

Please sign in to comment.