diff --git a/README.md b/README.md index a7ce708..5e3d3c6 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ return tryWith(new BufferedReader(new FileReader(path)), br -> ); ``` -### CompletableFuture.exceptionally(..) +### CompletableFuture's *exceptionally* [`CompletableFuture.exceptionally(..)`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html#exceptionally-java.util.function.Function-) is a very powerful but often overlooked tool. It allows to inject [partial exception handling](https://stackoverflow.com/questions/37032990/separated-exception-handling-of-a-completablefuture) @@ -182,6 +182,15 @@ particular case: future.exceptionally(partially(NoRouteToHostException.class, this::fallbackValueFor)) ``` +Other missing pieces in `CompletableFuture`'s API are `exceptionallyCompose` and `handleCompose`. Both can be seen as +a combination of `exceptionally` + `compose` and `handle` + `compose` respectively. They basically allow to supply +another `CompletableFuture` rather than concrete values directly. This is allows for asynchronous fallbacks: + +```java +exceptionallyCompose(users.find(name), e -> archive.find(name)) +``` + + ## Getting Help If you have questions, concerns, bug reports, etc., please file an issue in this repository's [Issue Tracker](../../issues). @@ -195,4 +204,5 @@ more details, check the [contribution guidelines](CONTRIBUTING.md). - [Lombok's `@SneakyThrows`](https://projectlombok.org/features/SneakyThrows.html) - [Durian's Errors](https://github.com/diffplug/durian) +- [Spotify's Completable Futures](https://github.com/spotify/completable-futures) diff --git a/src/main/java/org/zalando/fauxpas/FauxPas.java b/src/main/java/org/zalando/fauxpas/FauxPas.java index 851ab6f..278069f 100644 --- a/src/main/java/org/zalando/fauxpas/FauxPas.java +++ b/src/main/java/org/zalando/fauxpas/FauxPas.java @@ -1,9 +1,12 @@ package org.zalando.fauxpas; -import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.function.BiFunction; import java.util.function.Function; +import static java.util.function.Function.identity; + public final class FauxPas { FauxPas() { @@ -87,4 +90,19 @@ private static Throwable unpack(final Throwable throwable) { return throwable instanceof CompletionException && cause != null ? cause : throwable; } + public static CompletableFuture handleCompose(final CompletableFuture future, + final BiFunction> function) { + return future + .handle(function) + .thenCompose(identity()); + } + + public static CompletableFuture exceptionallyCompose(final CompletableFuture future, + final Function> function) { + return future + .thenApply(CompletableFuture::completedFuture) + .exceptionally(function) + .thenCompose(identity()); + } + } diff --git a/src/test/java/org/zalando/fauxpas/ExceptionallyComposeTest.java b/src/test/java/org/zalando/fauxpas/ExceptionallyComposeTest.java new file mode 100644 index 0000000..ca60e7f --- /dev/null +++ b/src/test/java/org/zalando/fauxpas/ExceptionallyComposeTest.java @@ -0,0 +1,26 @@ +package org.zalando.fauxpas; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.zalando.fauxpas.FauxPas.exceptionallyCompose; +import static org.zalando.fauxpas.FauxPas.partially; + +class ExceptionallyComposeTest { + + @Test + void shouldExceptionallyCompose() { + final CompletableFuture original = new CompletableFuture<>(); + final CompletableFuture unit = exceptionallyCompose(original, partially(e -> + completedFuture("result"))); + + original.completeExceptionally(new RuntimeException()); + + assertThat(unit.join(), is("result")); + } + +} diff --git a/src/test/java/org/zalando/fauxpas/ExceptionallyTest.java b/src/test/java/org/zalando/fauxpas/ExceptionallyTest.java index abd2d0c..105e9fb 100644 --- a/src/test/java/org/zalando/fauxpas/ExceptionallyTest.java +++ b/src/test/java/org/zalando/fauxpas/ExceptionallyTest.java @@ -10,13 +10,16 @@ import java.util.function.Function; import java.util.function.Predicate; +import static java.util.concurrent.CompletableFuture.completedFuture; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; +import static org.zalando.fauxpas.FauxPas.exceptionallyCompose; import static org.zalando.fauxpas.FauxPas.partially; class ExceptionallyTest { @@ -31,6 +34,19 @@ void shouldReturnResult() { assertThat(unit.join(), is("result")); } + @Test + void shouldCascade() { + final CompletableFuture original = new CompletableFuture<>(); + final CompletableFuture unit = original.exceptionally(partially(e -> { + throw new IllegalStateException(); + })); + + original.completeExceptionally(new IllegalArgumentException()); + + final CompletionException thrown = assertThrows(CompletionException.class, unit::join); + assertThat(thrown.getCause(), is(instanceOf(IllegalStateException.class))); + } + @Test void shouldUseFallbackWhenExplicitlyCompletedExceptionally() { final CompletableFuture original = new CompletableFuture<>(); diff --git a/src/test/java/org/zalando/fauxpas/HandleComposeTest.java b/src/test/java/org/zalando/fauxpas/HandleComposeTest.java new file mode 100644 index 0000000..856093d --- /dev/null +++ b/src/test/java/org/zalando/fauxpas/HandleComposeTest.java @@ -0,0 +1,38 @@ +package org.zalando.fauxpas; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.zalando.fauxpas.FauxPas.exceptionallyCompose; +import static org.zalando.fauxpas.FauxPas.handleCompose; +import static org.zalando.fauxpas.FauxPas.partially; + +class HandleComposeTest { + + @Test + void shouldHandleComposeResult() { + final CompletableFuture original = new CompletableFuture<>(); + final CompletableFuture unit = handleCompose(original, (s, throwable) -> + completedFuture("result")); + + original.complete("foo"); + + assertThat(unit.join(), is("result")); + } + + @Test + void shouldHandleComposeException() { + final CompletableFuture original = new CompletableFuture<>(); + final CompletableFuture unit = handleCompose(original, (s, throwable) -> + completedFuture("result")); + + original.completeExceptionally(new RuntimeException()); + + assertThat(unit.join(), is("result")); + } + +}