Skip to content

Commit 51759a5

Browse files
committed
Additional changes to IOUtils close and exception handling
1 parent 0d1b15c commit 51759a5

File tree

6 files changed

+224
-19
lines changed

6 files changed

+224
-19
lines changed

src/main/java/org/apache/commons/io/IOUtils.java

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@
4545
import java.nio.charset.Charset;
4646
import java.nio.file.Files;
4747
import java.util.ArrayList;
48-
import java.util.Arrays;
4948
import java.util.Collection;
5049
import java.util.List;
5150
import java.util.Objects;
5251
import java.util.function.Consumer;
52+
import java.util.stream.Stream;
5353

5454
import org.apache.commons.io.function.IOConsumer;
5555
import org.apache.commons.io.input.QueueInputStream;
@@ -394,6 +394,25 @@ public static void close(final Closeable... closeables) throws IOException {
394394
IOConsumer.forEach(closeables, IOUtils::close);
395395
}
396396

397+
/**
398+
* Closes the entries in the given {@link Stream<T>} as null-safe operations,
399+
* and closes the underlying {@code Stream}.
400+
*
401+
* @param <T> The element type.
402+
* @param closeables The resource(s) to close, may be null.
403+
* @throws IOExceptionList if an I/O error occurs.
404+
* @since 2.12.0
405+
*/
406+
public static <T extends Closeable> void close(final Stream<T> closeables) throws IOExceptionList {
407+
if (closeables != null) {
408+
try {
409+
IOConsumer.forEachIndexed(closeables, IOUtils::close);
410+
} finally {
411+
closeables.close();
412+
}
413+
}
414+
}
415+
397416
/**
398417
* Closes the given {@link Closeable} as a null-safe operation.
399418
*
@@ -415,13 +434,36 @@ public static void close(final Closeable closeable, final IOConsumer<IOException
415434
}
416435

417436
/**
418-
* Closes the given {@link Closeable} as a null-safe operation.
437+
* Closes the entries in the given {@link Stream<T>} as null-safe operations,
438+
* and closes the underlying {@code Stream}.
419439
*
440+
* @param <T> The element type.
441+
* @param consumer Consume the IOException thrown by {@link Closeable#close()}.
420442
* @param closeables The resource(s) to close, may be null.
443+
* @throws IOException if an I/O error occurs.
444+
*/
445+
public static <T extends Closeable> void close(final IOConsumer<IOException> consumer, final Stream<T> closeables) throws IOException {
446+
if (closeables != null) {
447+
try {
448+
close(closeables);
449+
} catch (final IOException e) {
450+
if (consumer != null) {
451+
consumer.accept(e);
452+
}
453+
} finally {
454+
closeables.close();
455+
}
456+
}
457+
}
458+
459+
/**
460+
* Closes the given {@link Closeable} as a null-safe operation.
461+
*
421462
* @param consumer Consume the IOException thrown by {@link Closeable#close()}.
463+
* @param closeables The resource(s) to close, may be null.
422464
* @throws IOException if an I/O error occurs.
423465
*/
424-
public static void close(final Closeable[] closeables, final IOConsumer<IOException> consumer) throws IOException {
466+
public static void close(final IOConsumer<IOException> consumer, final Closeable... closeables) throws IOException {
425467
if (closeables != null) {
426468
try {
427469
close(closeables);
@@ -538,7 +580,7 @@ public static void closeQuietly(final Closeable closeable) {
538580
*/
539581
public static void closeQuietly(final Closeable... closeables) {
540582
if (closeables != null) {
541-
Arrays.stream(closeables).forEach(IOUtils::closeQuietly);
583+
IOConsumer.forEachQuietly(closeables, IOUtils::closeQuietly);
542584
}
543585
}
544586

@@ -561,6 +603,64 @@ public static void closeQuietly(final Closeable closeable, final Consumer<IOExce
561603
}
562604
}
563605

606+
/**
607+
* Closes the given {@link Stream<T>} as a null-safe operation while consuming IOException by the given {@code consumer},
608+
* and closes the underlying {@code Stream}.
609+
*
610+
* @param <T> The element type.
611+
* @param closeables The resource(s) to close, may be null.
612+
* @since 2.12.0
613+
*/
614+
public static <T extends Closeable> void closeQuietly(final Stream<T> closeables) {
615+
closeQuietly(null, closeables);
616+
}
617+
618+
/**
619+
* Closes the given {@link Stream<T>} as a null-safe operation while consuming IOException by the given {@code consumer},
620+
* and closes the underlying {@code Stream}.
621+
*
622+
* @param <T> The element type.
623+
* @param consumer Consume the IOException thrown by {@link Closeable#close()}.
624+
* @param closeables The resource(s) to close, may be null.
625+
* @since 2.12.0
626+
*/
627+
public static <T extends Closeable> void closeQuietly(final Consumer<IOException> consumer, final Stream<T> closeables) {
628+
if (closeables != null) {
629+
try {
630+
close(closeables);
631+
} catch (final IOException e) {
632+
if (consumer != null) {
633+
consumer.accept(e);
634+
}
635+
} finally {
636+
try {
637+
closeables.close();
638+
} catch (Exception e) {
639+
// Do nothing.
640+
}
641+
}
642+
}
643+
}
644+
645+
/**
646+
* Closes the given {@link Closeable}s as a null-safe operation while consuming IOException by the given {@code consumer}.
647+
*
648+
* @param consumer Consume the IOException thrown by {@link Closeable#close()}.
649+
* @param closeables The resource(s) to close, may be null.
650+
* @since 2.12.0
651+
*/
652+
public static void closeQuietly(final Consumer<IOException> consumer, final Closeable... closeables) {
653+
if (closeables != null) {
654+
try {
655+
close(closeables);
656+
} catch (final IOException e) {
657+
if (consumer != null) {
658+
consumer.accept(e);
659+
}
660+
}
661+
}
662+
}
663+
564664
/**
565665
* Closes an {@code InputStream} unconditionally.
566666
* <p>

src/main/java/org/apache/commons/io/function/IOConsumer.java

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
package org.apache.commons.io.function;
1919

2020
import java.io.IOException;
21+
import java.io.UncheckedIOException;
2122
import java.util.Objects;
23+
import java.util.Optional;
2224
import java.util.function.Consumer;
2325
import java.util.stream.Stream;
2426

@@ -39,6 +41,29 @@ public interface IOConsumer<T> {
3941
*/
4042
IOConsumer<?> NOOP_IO_CONSUMER = t -> {/* noop */};
4143

44+
/**
45+
* Wraps an {@code IOConsumer} inside of a {@link Consumer}
46+
* that throws {@link UncheckedIOException} for any {@link IOException}s
47+
* that are thrown by the underlying {@code IOConsumer}.
48+
*
49+
* @param <T> The element type.
50+
* @param consumer The {@code IOConsumer} to wrap.
51+
* @return a {@code Consumer} that wraps the given {@code IOConsumer}.
52+
* @since 2.12.0
53+
*/
54+
static <T> Consumer<T> wrap(IOConsumer<T> consumer) {
55+
return new Consumer<T>() {
56+
@Override
57+
public void accept(T t) {
58+
try {
59+
consumer.accept(t);
60+
} catch (IOException e) {
61+
throw new UncheckedIOException(e);
62+
}
63+
}
64+
};
65+
}
66+
4267
/**
4368
* Performs an action for each element of this stream.
4469
*
@@ -52,6 +77,46 @@ static <T> void forEach(final T[] array, final IOConsumer<T> action) throws IOEx
5277
IOStreams.forEach(IOStreams.of(array), action);
5378
}
5479

80+
/**
81+
* Performs an action for each element of this array, returning
82+
* a {@link Optional} that either contains an {@link IOException}
83+
* if one occurred, or {@link Optional#empty()}.
84+
*
85+
* @param <T> The element type.
86+
* @param array The input to stream.
87+
* @param action The action to apply to each input element.
88+
* @return a {@code Optional} that may wrap a {@code IOException}.
89+
* @since 2.12.0
90+
*/
91+
static <T> Optional<IOException> forEachQuietly(final T[] array, final IOConsumer<T> action) {
92+
try {
93+
IOStreams.forEach(IOStreams.of(array), action);
94+
return Optional.empty();
95+
} catch (IOException e) {
96+
return Optional.of(e);
97+
}
98+
}
99+
100+
/**
101+
* Performs an action for each element of this stream, returning
102+
* a {@link Optional} that either contains an {@link IOExceptionList}
103+
* if one occurred, or {@link Optional#empty()}.
104+
*
105+
* @param <T> The element type.
106+
* @param stream The input to stream.
107+
* @param action The action to apply to each input element.
108+
* @return a {@code Optional} that may wrap a {@code IOExceptionList}.
109+
* @since 2.12.0
110+
*/
111+
static <T> Optional<IOExceptionList> forEachIndexedQuietly(final Stream<T> stream, final IOConsumer<T> action) {
112+
try {
113+
IOStreams.forEachIndexed(stream, action, IOIndexedException::new);
114+
return Optional.empty();
115+
} catch (IOExceptionList e) {
116+
return Optional.of(e);
117+
}
118+
}
119+
55120
/**
56121
* Performs an action for each element of this stream.
57122
*
@@ -85,13 +150,25 @@ static <T> IOConsumer<T> noop() {
85150
*/
86151
void accept(T t) throws IOException;
87152

153+
/**
154+
* Returns this {@code IOConsumer} wrapped inside of a {@link Consumer}
155+
* that throws {@link UncheckedIOException} for any {@link IOException}s
156+
* that are thrown by this {@code IOConsumer}.
157+
*
158+
* @return a {@code Consumer} that wraps this {@code IOConsumer}.
159+
* @since 2.12.0
160+
*/
161+
default Consumer<T> asConsumer() {
162+
return wrap(this);
163+
}
164+
88165
/**
89166
* Returns a composed {@code IOConsumer} that performs, in sequence, this operation followed by the {@code after}
90167
* operation. If performing either operation throws an exception, it is relayed to the caller of the composed operation.
91168
* If performing this operation throws an exception, the {@code after} operation will not be performed.
92169
*
93170
* @param after the operation to perform after this operation
94-
* @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} operation
171+
* @return a composed {@code IOConsumer} that performs in sequence this operation followed by the {@code after} operation
95172
* @throws NullPointerException if {@code after} is null
96173
*/
97174
default IOConsumer<T> andThen(final IOConsumer<? super T> after) {

src/main/java/org/apache/commons/io/function/IOStreams.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,27 @@ static <T> void forEach(final Stream<T> stream, final IOConsumer<T> action) thro
5252

5353
static <T> void forEachIndexed(final Stream<T> stream, final IOConsumer<T> action, final BiFunction<Integer, IOException, IOException> exSupplier)
5454
throws IOExceptionList {
55-
final AtomicReference<List<IOException>> causeList = new AtomicReference<>();
55+
final AtomicReference<List<Throwable>> causeList = new AtomicReference<>();
5656
final AtomicInteger index = new AtomicInteger();
57-
stream.forEach(e -> {
58-
try {
59-
action.accept(e);
60-
} catch (final IOException ioex) {
61-
if (causeList.get() == null) {
62-
causeList.set(new ArrayList<>());
57+
try {
58+
stream.forEach(e -> {
59+
try {
60+
action.accept(e);
61+
} catch (final IOException ioex) {
62+
if (causeList.get() == null) {
63+
causeList.set(new ArrayList<>());
64+
}
65+
causeList.get().add(exSupplier.apply(index.get(), ioex));
6366
}
64-
causeList.get().add(exSupplier.apply(index.get(), ioex));
67+
index.incrementAndGet();
68+
});
69+
}
70+
catch (Throwable t) {
71+
if (causeList.get() == null) {
72+
causeList.set(new ArrayList<>());
6573
}
66-
index.incrementAndGet();
67-
});
74+
causeList.get().add(t);
75+
}
6876
IOExceptionList.checkEmpty(causeList.get(), "forEach");
6977
}
70-
7178
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public List<Observer> getObservers() {
186186
}
187187

188188
/**
189-
* Notifies the observers by invoking {@link Observer#finished()}.
189+
* Notifies the observers by invoking {@link Observer#closed()}.
190190
*
191191
* @throws IOException Some observer has thrown an exception, which is being passed down.
192192
*/

src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.apache.commons.io.IOExceptionList;
3030
import org.apache.commons.io.IOIndexedException;
31+
import org.apache.commons.io.IOUtils;
3132
import org.apache.commons.io.function.IOConsumer;
3233

3334
/**
@@ -102,7 +103,7 @@ public Writer append(final CharSequence csq, final int start, final int end) thr
102103

103104
@Override
104105
public void close() throws IOException {
105-
IOConsumer.forEachIndexed(writers(), Writer::close);
106+
IOUtils.close(writers());
106107
}
107108

108109
/**

src/test/java/org/apache/commons/io/IOUtilsTest.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2121
import static org.junit.jupiter.api.Assertions.assertEquals;
2222
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2324
import static org.junit.jupiter.api.Assertions.assertNotNull;
2425
import static org.junit.jupiter.api.Assertions.assertNotSame;
2526
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -57,8 +58,9 @@
5758
import java.nio.file.Files;
5859
import java.nio.file.Path;
5960
import java.util.Arrays;
61+
import java.util.Collection;
62+
import java.util.HashSet;
6063
import java.util.List;
61-
6264
import org.apache.commons.io.function.IOConsumer;
6365
import org.apache.commons.io.input.CircularInputStream;
6466
import org.apache.commons.io.input.NullInputStream;
@@ -67,7 +69,9 @@
6769
import org.apache.commons.io.output.NullOutputStream;
6870
import org.apache.commons.io.output.StringBuilderWriter;
6971
import org.apache.commons.io.test.TestUtils;
72+
import org.apache.commons.io.test.ThrowOnCloseInputStream;
7073
import org.apache.commons.io.test.ThrowOnCloseReader;
74+
import org.apache.commons.io.test.ThrowOnCloseWriter;
7175
import org.junit.jupiter.api.BeforeEach;
7276
import org.junit.jupiter.api.Disabled;
7377
import org.junit.jupiter.api.Test;
@@ -380,6 +384,22 @@ public void testCloseMulti() {
380384
() -> IOUtils.close(nullCloseable, new ThrowOnCloseReader(new StringReader("s"))));
381385
}
382386

387+
@Test
388+
public void testCloseMultiConsumer() {
389+
final Collection<IOException> exceptionCollection = new HashSet<>();
390+
final IOConsumer<IOException> checkConsumer = i -> {
391+
exceptionCollection.add(i);
392+
};
393+
394+
final Closeable[] closeables = {null, new ThrowOnCloseInputStream(), new ThrowOnCloseReader(), new ThrowOnCloseWriter()};
395+
assertDoesNotThrow(() -> IOUtils.close(checkConsumer, closeables));
396+
assertEquals(exceptionCollection.size(), 1);
397+
398+
final IOException exception = exceptionCollection.iterator().next();
399+
assertInstanceOf(IOExceptionList.class, exception);
400+
assertEquals(((IOExceptionList)exception).getCauseList().size(), 3);
401+
}
402+
383403
@Test
384404
public void testCloseQuietly_AllCloseableIOException() {
385405
final Closeable closeable = () -> {

0 commit comments

Comments
 (0)