This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
commit cf7ab247afdada7b973d65a62c3744f4a6e54192 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sat Nov 9 10:55:42 2024 -0500 Add ProxyInputStream.AbstractBuilder Supports setting a consumer for ProxyInputStream.afterRead(int). --- .../apache/commons/io/input/ProxyInputStream.java | 103 +++++++++++++++++---- .../commons/io/input/ProxyInputStreamTest.java | 61 ++++++++++-- .../apache/commons/io/test/CustomIOException.java | 44 +++++++++ 3 files changed, 182 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/apache/commons/io/input/ProxyInputStream.java b/src/main/java/org/apache/commons/io/input/ProxyInputStream.java index 4dee0cc0a..8c359c4ac 100644 --- a/src/main/java/org/apache/commons/io/input/ProxyInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ProxyInputStream.java @@ -23,8 +23,10 @@ import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.build.AbstractStreamBuilder; import org.apache.commons.io.function.Erase; import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.io.function.IOIntConsumer; /** * A proxy stream which acts as a {@link FilterInputStream}, by passing all method calls on to the proxied stream, not changing which methods are called. @@ -44,6 +46,50 @@ import org.apache.commons.io.function.IOConsumer; */ public abstract class ProxyInputStream extends FilterInputStream { + /** + * Abstracts builder properties for subclasses. + * + * @param <T> The InputStream type. + * @param <B> The builder type. + * @since 2.18.0 + */ + protected static abstract class AbstractBuilder<T, B extends AbstractStreamBuilder<T, B>> extends AbstractStreamBuilder<T, B> { + + private IOIntConsumer afterRead; + + /** + * Gets the {@link ProxyInputStream#afterRead(int)} consumer. + * + * @return the {@link ProxyInputStream#afterRead(int)} consumer. + */ + public IOIntConsumer getAfterRead() { + return afterRead; + } + + /** + * Sets the {@link ProxyInputStream#afterRead(int)} behavior, null resets to a NOOP. + * <p> + * Setting this value causes the {@link ProxyInputStream#afterRead(int) afterRead} method to delegate to the given consumer. + * </p> + * <p> + * If a subclass overrides {@link ProxyInputStream#afterRead(int) afterRead} and does not call {@code super.afterRead(int)}, then the given consumer is + * not called. + * </p> + * <p> + * This does <em>not</em> override a {@code ProxyInputStream} subclass' implementation of the {@link ProxyInputStream#afterRead(int)} method, it can + * supplement it. + * </p> + * + * @param afterRead the {@link ProxyInputStream#afterRead(int)} behavior. + * @return this instance. + */ + public B setAfterRead(final IOIntConsumer afterRead) { + this.afterRead = afterRead; + return asThis(); + } + + } + /** * Tracks whether {@link #close()} has been called or not. */ @@ -54,50 +100,67 @@ public abstract class ProxyInputStream extends FilterInputStream { */ private final IOConsumer<IOException> exceptionHandler; + private final IOIntConsumer afterRead; + /** * Constructs a new ProxyInputStream. * * @param proxy the InputStream to proxy. */ public ProxyInputStream(final InputStream proxy) { - // the proxy is stored in a protected superclass variable named 'in'. - this(proxy, Erase::rethrow); + // the delegate is stored in a protected superclass variable named 'in'. + super(proxy); + this.exceptionHandler = Erase::rethrow; + this.afterRead = IOIntConsumer.NOOP; } /** - * Constructs a new ProxyInputStream for testing. + * Constructs a new ProxyInputStream. + * + * @param builder How to build an instance. + * @throws IOException if an I/O error occurs. + * @since 2.18.0 + */ + @SuppressWarnings("resource") + protected ProxyInputStream(final AbstractBuilder<?, ?> builder) throws IOException { + // the delegate is stored in a protected superclass instance variable named 'in'. + this(builder.getInputStream(), builder); + } + + /** + * Constructs a new ProxyInputStream. * * @param proxy the InputStream to proxy. - * @param exceptionHandler the exception handler. + * @param builder How to build an instance. + * @since 2.18.0 */ - ProxyInputStream(final InputStream proxy, final IOConsumer<IOException> exceptionHandler) { - // the proxy is stored in a protected superclass instance variable named 'in'. + protected ProxyInputStream(final InputStream proxy, final AbstractBuilder<?, ?> builder) { + // the delegate is stored in a protected superclass instance variable named 'in'. super(proxy); - this.exceptionHandler = exceptionHandler; + this.exceptionHandler = Erase::rethrow; + this.afterRead = builder.getAfterRead() != null ? builder.getAfterRead() : IOIntConsumer.NOOP; } /** - * Invoked by the {@code read} methods after the proxied call has returned - * successfully. The number of bytes returned to the caller (or {@link IOUtils#EOF EOF} if - * the end of stream was reached) is given as an argument. + * Called by the {@code read} methods after the proxied call has returned successfully. The argument is the number of bytes returned to the caller or + * {@link IOUtils#EOF EOF} if the end of stream was reached. * <p> - * Subclasses can override this method to add common post-processing - * functionality without having to override all the read methods. - * The default implementation does nothing. + * The default delegates to the consumer given to {@link AbstractBuilder#setAfterRead(IOIntConsumer)}. * </p> * <p> - * Note this method is <em>not</em> called from {@link #skip(long)} or - * {@link #reset()}. You need to explicitly override those methods if - * you want to add post-processing steps also to them. + * Alternatively, a subclasses can override this method to add post-processing functionality without having to override all the read methods. + * </p> + * <p> + * Note this method is <em>not</em> called from {@link #skip(long)} or {@link #reset()}. You need to explicitly override those methods if you want to add + * post-processing steps also to them. * </p> * - * @since 2.0 * @param n number of bytes read, or {@link IOUtils#EOF EOF} if the end of stream was reached. - * @throws IOException if the post-processing fails in a subclass. + * @throws IOException Thrown by a subclass or the consumer given to {@link AbstractBuilder#setAfterRead(IOIntConsumer)}. + * @since 2.0 */ - @SuppressWarnings("unused") // Possibly thrown from subclasses. protected void afterRead(final int n) throws IOException { - // no-op default + afterRead.accept(n); } /** diff --git a/src/test/java/org/apache/commons/io/input/ProxyInputStreamTest.java b/src/test/java/org/apache/commons/io/input/ProxyInputStreamTest.java index db047b1e0..2b5ba176f 100644 --- a/src/test/java/org/apache/commons/io/input/ProxyInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/ProxyInputStreamTest.java @@ -24,6 +24,8 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -34,9 +36,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractStreamBuilder; +import org.apache.commons.io.test.CustomIOException; import org.junit.jupiter.api.Test; /** @@ -48,6 +52,23 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { private static final class ProxyInputStreamFixture extends ProxyInputStream { + static class Builder extends ProxyInputStream.AbstractBuilder<ProxyInputStreamFixture, Builder> { + + @Override + public ProxyInputStreamFixture get() throws IOException { + return new ProxyInputStreamFixture(this); + } + + } + + static Builder builder() { + return new Builder(); + } + + ProxyInputStreamFixture(final Builder builder) throws IOException { + super(builder); + } + ProxyInputStreamFixture(final InputStream proxy) { super(proxy); } @@ -187,7 +208,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertEquals('c', found); found = inputStream.read(); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -228,7 +249,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertArrayEquals(new byte[] { 0, 0, 'a', 'b', 'c' }, dest); found = inputStream.read(dest, 2, 3); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -241,7 +262,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertArrayEquals(new byte[] { 'a', 'b', 'c', 0, 0 }, dest); found = inputStream.read(dest, 0, 5); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -258,7 +279,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertArrayEquals(new byte[] { 'c', 0, 0, 0, 0 }, dest); found = inputStream.read(dest, 0, 2); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -271,7 +292,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertArrayEquals(new byte[] { 'a', 'b', 'c', 0, 0 }, dest); found = inputStream.read(dest); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -288,7 +309,7 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { assertArrayEquals(new byte[] { 'c', 0 }, dest); found = inputStream.read(dest); assertEquals(-1, found); - testEos((T) inputStream); + testEos(inputStream); } } @@ -306,4 +327,32 @@ public class ProxyInputStreamTest<T extends ProxyInputStream> { } } + @Test + public void testSubclassAfterReadConsumer() throws Exception { + final byte[] hello = "Hello".getBytes(StandardCharsets.UTF_8); + final AtomicBoolean boolRef = new AtomicBoolean(); + // @formatter:off + try (ProxyInputStreamFixture bounded = ProxyInputStreamFixture.builder() + .setInputStream(new ByteArrayInputStream(hello)) + .setAfterRead(null) // should not blow up + .setAfterRead(i -> boolRef.set(true)) + .get()) { + IOUtils.consume(bounded); + } + // @formatter:on + assertTrue(boolRef.get()); + // Throwing + final String message = "test exception message"; + // @formatter:off + try (ProxyInputStreamFixture bounded = ProxyInputStreamFixture.builder() + .setInputStream(new ByteArrayInputStream(hello)) + .setAfterRead(i -> { + throw new CustomIOException(message); + }) + .get()) { + assertEquals(message, assertThrowsExactly(CustomIOException.class, () -> IOUtils.consume(bounded)).getMessage()); + } + // @formatter:on + } + } diff --git a/src/test/java/org/apache/commons/io/test/CustomIOException.java b/src/test/java/org/apache/commons/io/test/CustomIOException.java new file mode 100644 index 000000000..dcd4f7cfb --- /dev/null +++ b/src/test/java/org/apache/commons/io/test/CustomIOException.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.test; + +import java.io.IOException; + +/** + * A custom IOException for testing that an exact IOException is thrown. + */ +public class CustomIOException extends IOException { + + private static final long serialVersionUID = 1L; + + public CustomIOException() { + } + + public CustomIOException(final String message) { + super(message); + } + + public CustomIOException(final String message, final Throwable cause) { + super(message, cause); + } + + public CustomIOException(final Throwable cause) { + super(cause); + } + +}