Skip to content

Commit e0b3ba5

Browse files
committed
Functional methods for exception handling
1 parent bb73720 commit e0b3ba5

File tree

3 files changed

+148
-21
lines changed

3 files changed

+148
-21
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package au.com.williamhill.flywheel.util;
2+
3+
import java.util.function.*;
4+
5+
/**
6+
* Functional equivalent of a try-catch block, as well as complimentary utilities.
7+
*/
8+
public final class Exceptions {
9+
private Exceptions() {}
10+
11+
/**
12+
* A {@link Runnable} that may throw an exception.
13+
*/
14+
@FunctionalInterface
15+
public interface ThrowingRunnable extends ThrowingSupplier<Void> {
16+
void run() throws Exception;
17+
18+
@Override default Void create() throws Exception {
19+
run();
20+
return null;
21+
}
22+
}
23+
24+
/**
25+
* A {@link Supplier} that may throw an exception.
26+
*
27+
* @param <T> The return type.
28+
*/
29+
@FunctionalInterface
30+
public interface ThrowingSupplier<T> {
31+
T create() throws Exception;
32+
}
33+
34+
/**
35+
* Invokes a given block. If an exception is thrown, it is
36+
* first passed through the given {@code exceptionManager} mapping {@link Function} before being rethrown.
37+
*
38+
* @param <X> The exterior exception type.
39+
* @param runnable The runnable block.
40+
* @param exceptionMapper The exception mapper (from the interior exception to the exterior exception type).
41+
* @throws X The exterior exception if the {@code runnable} block faulted.
42+
*/
43+
public static <X extends Exception> void rethrow(ThrowingRunnable runnable, Function<Exception, X> exceptionMapper) throws X {
44+
rethrow((ThrowingSupplier<Void>) runnable, exceptionMapper);
45+
}
46+
47+
/**
48+
* Invokes a given supplier block returning the outcome if successful. Otherwise, if an exception is thrown, it is
49+
* first passed through the given {@code exceptionManager} mapping {@link Function} before being rethrown.
50+
*
51+
* @param <T> The return type.
52+
* @param <X> The exterior exception type.
53+
* @param supplier The supplier block.
54+
* @param exceptionMapper The exception mapper (from the interior exception to the exterior exception type).
55+
* @return The value returned by the {@code supplier} block.
56+
* @throws X The exterior exception if the {@code supplier} block faulted.
57+
*/
58+
public static <T, X extends Exception> T rethrow(ThrowingSupplier<T> supplier, Function<Exception, X> exceptionMapper) throws X {
59+
try {
60+
return supplier.create();
61+
} catch (Exception e) {
62+
throw exceptionMapper.apply(e);
63+
}
64+
}
65+
66+
/**
67+
* Obtains the root cause of a given exception.
68+
*
69+
* @param throwable The exception.
70+
* @return The root cause.
71+
*/
72+
public static Throwable getRootCause(Throwable throwable) {
73+
Throwable cause = throwable;
74+
while (cause.getCause() != null) {
75+
cause = cause.getCause();
76+
}
77+
return cause;
78+
}
79+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package au.com.williamhill.flywheel.util;
2+
3+
import static org.junit.Assert.*;
4+
5+
import org.junit.*;
6+
7+
import com.obsidiandynamics.assertion.*;
8+
9+
public final class ExceptionsTest {
10+
@Test
11+
public void testConformance() throws Exception {
12+
Assertions.assertUtilityClassWellDefined(Exceptions.class);
13+
}
14+
15+
private class TestException extends Exception {
16+
private static final long serialVersionUID = 1L;
17+
TestException(Throwable cause) { super(cause); }
18+
TestException(String m, Throwable cause) { super(m, cause); }
19+
}
20+
21+
@Test
22+
public void testReturnNoThrow() throws TestException {
23+
final String value = Exceptions.rethrow(() -> "ok", e -> new TestException(e));
24+
assertEquals("ok", value);
25+
}
26+
27+
@Test
28+
public void testReturnThrow() {
29+
final Exception cause = new Exception("boom");
30+
try {
31+
Exceptions.rethrow(() -> {
32+
throw cause;
33+
}, e -> new TestException("thrown", e));
34+
fail("Expected exception not thrown");
35+
} catch (TestException e) {
36+
assertEquals(cause, e.getCause());
37+
assertEquals("thrown", e.getMessage());
38+
}
39+
}
40+
41+
@Test
42+
public void testVoidNoThrow() throws TestException {
43+
Exceptions.rethrow(() -> {}, e -> new TestException(e));
44+
}
45+
46+
@Test
47+
public void testRootCause() {
48+
final Exception root = new Exception("root");
49+
final Exception intermediate = new Exception("intermediate", root);
50+
assertEquals(root, Exceptions.getRootCause(intermediate));
51+
}
52+
}

standalone/src/main/java/au/com/williamhill/flywheel/Launchpad.java

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
import com.obsidiandynamics.indigo.util.*;
1212

13+
import au.com.williamhill.flywheel.util.*;
14+
import au.com.williamhill.flywheel.util.Exceptions.ThrowingRunnable;
15+
import au.com.williamhill.flywheel.util.Exceptions.ThrowingSupplier;
16+
1317
public final class Launchpad {
1418
private static final String DEF_PROFILE = "conf/default";
1519

@@ -33,21 +37,17 @@ static final class LaunchpadException extends Exception {
3337
if (! profileYaml.exists()) {
3438
throw new LaunchpadException("Profile configuration " + profileYaml + " is missing", null);
3539
}
36-
37-
try {
38-
profile = Profile.fromFile(profileYaml);
39-
} catch (Exception e) {
40-
throw new LaunchpadException("Error reading profile", getRootCause(e));
41-
}
40+
41+
profile = rethrowLaunchpadException(() -> {
42+
return Profile.fromFile(profileYaml);
43+
}, "Error reading profile");
4244

4345
final StringBuilder sb = new StringBuilder();
4446

45-
try {
47+
rethrowLaunchpadException(() -> {
4648
sb.append("\n Flywheel version: ").append(FlywheelVersion.get());
4749
sb.append("\n Indigo version: ").append(IndigoVersion.get());
48-
} catch (IOException e) {
49-
throw new LaunchpadException("Error retrieving version", e);
50-
}
50+
}, "Error retrieving version");
5151

5252
sb.append("\n Properties:");
5353
for (Map.Entry<String, ?> entry : profile.properties.entrySet()) {
@@ -63,21 +63,17 @@ static final class LaunchpadException extends Exception {
6363
log.info(sb.toString());
6464
}
6565

66-
private static Throwable getRootCause(Throwable throwable) {
67-
Throwable cause = throwable;
68-
while (cause.getCause() != null) {
69-
cause = cause.getCause();
70-
}
71-
return cause;
66+
private static void rethrowLaunchpadException(ThrowingRunnable lambda, String message) throws LaunchpadException {
67+
rethrowLaunchpadException((ThrowingSupplier<Void>) lambda, message);
68+
}
69+
70+
private static <T> T rethrowLaunchpadException(ThrowingSupplier<T> lambda, String message) throws LaunchpadException {
71+
return Exceptions.rethrow(lambda, e -> new LaunchpadException(message, Exceptions.getRootCause(e)));
7272
}
7373

7474
public void launch(String[] args) throws LaunchpadException {
7575
for (Launcher launcher : profile.launchers) {
76-
try {
77-
launcher.launch(args);
78-
} catch (Exception e) {
79-
throw new LaunchpadException("Failed to launch " + launcher, getRootCause(e));
80-
}
76+
rethrowLaunchpadException(() -> launcher.launch(args), "Failed to launch " + launcher);
8177
}
8278
}
8379

0 commit comments

Comments
 (0)