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);
+    }
+
+}

Reply via email to