diff --git a/src/main/java/org/apache/commons/io/IOExceptionList.java b/src/main/java/org/apache/commons/io/IOExceptionList.java index e4343576078..d0db70a2b4a 100644 --- a/src/main/java/org/apache/commons/io/IOExceptionList.java +++ b/src/main/java/org/apache/commons/io/IOExceptionList.java @@ -18,6 +18,7 @@ package org.apache.commons.io; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -35,6 +36,30 @@ public class IOExceptionList extends IOException { private static final long serialVersionUID = 1L; + /** + * Unwinds a {@code IOExceptionList} into a {@link List} of {@link Throwable} + * containing all of the underlying {@code Throwable} instances using + * {@link #getCauseList()}. + * + * Any instances of {@code IOExceptionList} encountered will be recursively + * unwound as well, and the contents of their {@code #getCauseList()} will + * be included in the returned {@code List}. + * + * @param ioExceptionList The {@code IOExceptionList} to recursively unwind, + * may be null, and {@code IOExceptionList#getCauseList()} may be null or empty. + * @return A {@code List} containing all of the {@code Throwable} instances + * inside the given {@code IOExceptionList} using {@code IOExceptionList#getCauseList()}, + * this {@code List} will never contain instances of {@code IOExceptionList} itself. + * @since 2.12.0 + */ + public static List unwind(IOExceptionList ioExceptionList) { + if (ioExceptionList != null && !IOExceptionList.isEmpty(ioExceptionList.getCauseList())) { + return unwind(ioExceptionList.getCauseList()); + } else { + return Collections.emptyList(); + } + } + /** * Throws this exception if the list is not null or empty. * @@ -49,6 +74,20 @@ public static void checkEmpty(final List causeList, final O } } + private static List unwind(final List causeList) { + final List exceptions = new ArrayList<>(); + if (Objects.nonNull(causeList)) { + for (Throwable t : causeList) { + if (t instanceof IOExceptionList) { + exceptions.addAll(unwind((IOExceptionList)t)); + } else { + exceptions.add(t); + } + } + } + return exceptions; + } + private static boolean isEmpty(final List causeList) { return causeList == null || causeList.isEmpty(); } diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java index 6c6a007ffd5..8244ffa66cb 100644 --- a/src/main/java/org/apache/commons/io/IOUtils.java +++ b/src/main/java/org/apache/commons/io/IOUtils.java @@ -45,11 +45,11 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import java.util.stream.Stream; import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.input.QueueInputStream; @@ -394,6 +394,20 @@ public static void close(final Closeable... closeables) throws IOException { IOConsumer.forEach(closeables, IOUtils::close); } + /** + * Closes the entries in the given {@link Stream} as null-safe operations. + * + * @param The element type. + * @param closeables The resource(s) to close, may be null or empty. + * @throws IOExceptionList if an I/O error occurs. + * @since 2.12.0 + */ + public static void close(final Stream closeables) throws IOExceptionList { + if (closeables != null) { + IOConsumer.forEachIndexed(closeables.filter(Objects::nonNull), IOUtils::close); + } + } + /** * Closes the given {@link Closeable} as a null-safe operation. * @@ -414,6 +428,43 @@ public static void close(final Closeable closeable, final IOConsumer The element type. + * @param consumer Consume the IOException thrown by {@link Closeable#close()}. + * @param closeables The resource(s) to close, may be null or empty. + * @throws IOException if an I/O error occurs. + */ + public static void close(final IOConsumer consumer, final Stream closeables) throws IOException { + try { + close(closeables); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param consumer Consume the IOException thrown by {@link Closeable#close()}. + * @param closeables The resource(s) to close, may be null. + * @throws IOException if an I/O error occurs. + */ + public static void close(final IOConsumer consumer, final Closeable... closeables) throws IOException { + if (closeables != null) { + try { + close(closeables); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + } + /** * Closes a URLConnection. * @@ -519,7 +570,7 @@ public static void closeQuietly(final Closeable closeable) { */ public static void closeQuietly(final Closeable... closeables) { if (closeables != null) { - Arrays.stream(closeables).forEach(IOUtils::closeQuietly); + IOConsumer.forEachQuietly(closeables, IOUtils::closeQuietly); } } @@ -542,6 +593,54 @@ public static void closeQuietly(final Closeable closeable, final Consumer The element type. + * @param closeables The resource(s) to close, may be null or empty. + * @since 2.12.0 + */ + public static void closeQuietly(final Stream closeables) { + closeQuietly(null, closeables); + } + + /** + * Closes the given {@link Stream} as a null-safe operation while consuming IOException by the given {@code consumer}. + * + * @param The element type. + * @param consumer Consume the IOException thrown by {@link Closeable#close()}, may be null. + * @param closeables The resource(s) to close, may be null or empty. + * @since 2.12.0 + */ + public static void closeQuietly(final Consumer consumer, final Stream closeables) { + try { + close(closeables); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + + /** + * Closes the given {@link Closeable}s as a null-safe operation while consuming IOException by the given {@code consumer}. + * + * @param consumer Consume the IOException thrown by {@link Closeable#close()}. + * @param closeables The resource(s) to close, may be null. + * @since 2.12.0 + */ + public static void closeQuietly(final Consumer consumer, final Closeable... closeables) { + if (closeables != null) { + try { + close(closeables); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + } + /** * Closes an {@code InputStream} unconditionally. *

diff --git a/src/main/java/org/apache/commons/io/function/IOConsumer.java b/src/main/java/org/apache/commons/io/function/IOConsumer.java index 662cd34d254..02a45d5e597 100644 --- a/src/main/java/org/apache/commons/io/function/IOConsumer.java +++ b/src/main/java/org/apache/commons/io/function/IOConsumer.java @@ -18,12 +18,15 @@ package org.apache.commons.io.function; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Objects; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; import org.apache.commons.io.IOExceptionList; import org.apache.commons.io.IOIndexedException; +import org.apache.commons.io.UncheckedIOExceptions; /** * Like {@link Consumer} but throws {@link IOException}. @@ -39,6 +42,52 @@ public interface IOConsumer { */ IOConsumer NOOP_IO_CONSUMER = t -> {/* noop */}; + /** + * Wraps an {@code IOConsumer} inside of a {@link Consumer} + * that throws {@link UncheckedIOException} for any {@link IOException}s + * that are thrown by the underlying {@code IOConsumer}. + * + * @param The element type. + * @param consumer The {@code IOConsumer} to wrap. + * @return a {@code Consumer} that wraps the given {@code IOConsumer}. + * @since 2.12.0 + */ + static Consumer asConsumer(IOConsumer consumer) { + return new Consumer() { + @Override + public void accept(T t) { + try { + consumer.accept(t); + } catch (IOException e) { + throw UncheckedIOExceptions.create(String.format("%s thrown from %s", e.getClass().getName(), String.valueOf(consumer)), e); + } + } + }; + } + + /** + * Wraps a {@link Consumer} inside of a {@code IOConsumer} + * that catches {@link UncheckedIOException}s that are thrown by the underlying + * {@code IOConsumer} and rethrows them as {@link IOException} + * + * @param The element type. + * @param consumer The {@code Consumer} to wrap. + * @return a {@code IOConsumer} that wraps the given {@code Consumer}. + * @since 2.12.0 + */ + static IOConsumer wrap(Consumer consumer) { + return new IOConsumer() { + @Override + public void accept(T t) throws IOException { + try { + consumer.accept(t); + } catch (UncheckedIOException e) { + throw e.getCause() == null ? new IOException(e) : e.getCause(); + } + } + }; + } + /** * Performs an action for each element of this stream. * @@ -52,6 +101,46 @@ static void forEach(final T[] array, final IOConsumer action) throws IOEx IOStreams.forEach(IOStreams.of(array), action); } + /** + * Performs an action for each element of this array, returning + * a {@link Optional} that either contains an {@link IOException} + * if one occurred, or {@link Optional#empty()}. + * + * @param The element type. + * @param array The input to stream. + * @param action The action to apply to each input element. + * @return a {@code Optional} that may wrap a {@code IOException}. + * @since 2.12.0 + */ + static Optional forEachQuietly(final T[] array, final IOConsumer action) { + try { + IOStreams.forEach(IOStreams.of(array), action); + return Optional.empty(); + } catch (IOException e) { + return Optional.of(e); + } + } + + /** + * Performs an action for each element of this stream, returning + * a {@link Optional} that either contains an {@link IOExceptionList} + * if one occurred, or {@link Optional#empty()}. + * + * @param The element type. + * @param stream The input to stream. + * @param action The action to apply to each input element. + * @return a {@code Optional} that may wrap a {@code IOExceptionList}. + * @since 2.12.0 + */ + static Optional forEachIndexedQuietly(final Stream stream, final IOConsumer action) { + try { + IOStreams.forEachIndexed(stream, action, IOIndexedException::new); + return Optional.empty(); + } catch (IOExceptionList e) { + return Optional.of(e); + } + } + /** * Performs an action for each element of this stream. * @@ -91,7 +180,7 @@ static IOConsumer noop() { * If performing this operation throws an exception, the {@code after} operation will not be performed. * * @param after the operation to perform after this operation - * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} operation + * @return a composed {@code IOConsumer} that performs in sequence this operation followed by the {@code after} operation * @throws NullPointerException if {@code after} is null */ default IOConsumer andThen(final IOConsumer after) { diff --git a/src/main/java/org/apache/commons/io/function/IOStreams.java b/src/main/java/org/apache/commons/io/function/IOStreams.java index 52654cbc48b..557663e5467 100644 --- a/src/main/java/org/apache/commons/io/function/IOStreams.java +++ b/src/main/java/org/apache/commons/io/function/IOStreams.java @@ -19,10 +19,13 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import java.util.function.Supplier; import java.util.stream.Stream; import org.apache.commons.io.IOExceptionList; @@ -52,20 +55,50 @@ static void forEach(final Stream stream, final IOConsumer action) thro static void forEachIndexed(final Stream stream, final IOConsumer action, final BiFunction exSupplier) throws IOExceptionList { - final AtomicReference> causeList = new AtomicReference<>(); + final LazyAtomicReference> causeList = new LazyAtomicReference<>(() -> new ArrayList<>()); final AtomicInteger index = new AtomicInteger(); - stream.forEach(e -> { - try { - action.accept(e); - } catch (final IOException ioex) { - if (causeList.get() == null) { - causeList.set(new ArrayList<>()); + try { + stream.forEach(e -> { + try { + action.accept(e); + } catch (final IOExceptionList ioexl) { + final Collection exceptions = IOExceptionList.unwind(ioexl); + for (final Throwable t : exceptions) { + if (t instanceof IOException) { + causeList.getLazy().add(exSupplier.apply(index.get(), (IOException)t)); + } else { + causeList.getLazy().add(exSupplier.apply(index.get(), new IOException(t))); + } + } + } catch (final IOException ioex) { + causeList.getLazy().add(exSupplier.apply(index.get(), ioex)); + } catch (final Throwable t) { + causeList.getLazy().add(exSupplier.apply(index.get(), new IOException(t))); } - causeList.get().add(exSupplier.apply(index.get(), ioex)); - } - index.incrementAndGet(); - }); + index.incrementAndGet(); + }); + } + catch (Throwable t) { + causeList.getLazy().add(exSupplier.apply(index.get(), new IOException(t))); + } IOExceptionList.checkEmpty(causeList.get(), "forEach"); } + private static class LazyAtomicReference extends AtomicReference { + + private static final long serialVersionUID = 1L; + private final Supplier supplier; + + public LazyAtomicReference(Supplier supplier) { + Objects.requireNonNull(supplier); + this.supplier = supplier; + } + + public final T getLazy() { + if (Objects.isNull(get())) { + set(supplier.get()); + } + return get(); + } + } } diff --git a/src/main/java/org/apache/commons/io/input/ObservableInputStream.java b/src/main/java/org/apache/commons/io/input/ObservableInputStream.java index d57eb0585db..795ac07b8b5 100644 --- a/src/main/java/org/apache/commons/io/input/ObservableInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ObservableInputStream.java @@ -186,7 +186,7 @@ public List getObservers() { } /** - * Notifies the observers by invoking {@link Observer#finished()}. + * Notifies the observers by invoking {@link Observer#closed()}. * * @throws IOException Some observer has thrown an exception, which is being passed down. */ diff --git a/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java index 08e798038b2..bebf7978f18 100644 --- a/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java +++ b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java @@ -28,6 +28,7 @@ import org.apache.commons.io.IOExceptionList; import org.apache.commons.io.IOIndexedException; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.function.IOConsumer; /** @@ -102,7 +103,7 @@ public Writer append(final CharSequence csq, final int start, final int end) thr @Override public void close() throws IOException { - IOConsumer.forEachIndexed(writers(), Writer::close); + IOUtils.close(writers()); } /** diff --git a/src/test/java/org/apache/commons/io/IOUtilsTest.java b/src/test/java/org/apache/commons/io/IOUtilsTest.java index 9b7214128c9..788c2ea9eea 100644 --- a/src/test/java/org/apache/commons/io/IOUtilsTest.java +++ b/src/test/java/org/apache/commons/io/IOUtilsTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; @@ -57,8 +58,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; - import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.input.CircularInputStream; import org.apache.commons.io.input.NullInputStream; @@ -67,7 +69,9 @@ import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.io.output.StringBuilderWriter; import org.apache.commons.io.test.TestUtils; +import org.apache.commons.io.test.ThrowOnCloseInputStream; import org.apache.commons.io.test.ThrowOnCloseReader; +import org.apache.commons.io.test.ThrowOnCloseWriter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -380,6 +384,22 @@ public void testCloseMulti() { () -> IOUtils.close(nullCloseable, new ThrowOnCloseReader(new StringReader("s")))); } + @Test + public void testCloseMultiConsumer() { + final Collection exceptionCollection = new HashSet<>(); + final IOConsumer checkConsumer = i -> { + exceptionCollection.add(i); + }; + + final Closeable[] closeables = {null, new ThrowOnCloseInputStream(), new ThrowOnCloseReader(), new ThrowOnCloseWriter()}; + assertDoesNotThrow(() -> IOUtils.close(checkConsumer, closeables)); + assertEquals(exceptionCollection.size(), 1); + + final IOException exception = exceptionCollection.iterator().next(); + assertInstanceOf(IOExceptionList.class, exception); + assertEquals(((IOExceptionList)exception).getCauseList().size(), 3); + } + @Test public void testCloseQuietly_AllCloseableIOException() { final Closeable closeable = () -> {