Skip to content

Commit aef38e9

Browse files
19888 enforce explicit cause declaration in fluent exception api (#6)
* Make changes to espresso exceptions * Refactor to two stage approach. Add test. * Fix deprecated usages within the espresso repo. spotless. * update readme with nexus link * bump version * move methods to abstract * make methods public
1 parent cc9b272 commit aef38e9

11 files changed

Lines changed: 169 additions & 36 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Create your feature branch and start working. To test how the changes you are ma
88
./gradlew clean build
99
./gradlew publish
1010
```
11-
This will publish a snapshot of the library to our internal Maven repository with the name `<version>-SNAPSHOT`
11+
This will publish a snapshot of the library to our internal Maven repository at https://repository.goziro.com/ with
12+
the name `<version>-SNAPSHOT`
1213

1314
You can then go to the other project, and change the version that project consumes to match your snapshot.
1415

src/main/java/com/ziro/espresso/fluent/exceptions/AbstractFluentExceptionSupport.java

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,45 @@
1111
* Provides fluent support to any type of throwable.
1212
*/
1313
@NonNullByDefault
14-
public abstract class AbstractFluentExceptionSupport<T extends Throwable> {
14+
public abstract class AbstractFluentExceptionSupport<T extends Throwable> implements ExceptionDetailsStage<T> {
1515

16-
private static final String DEFAULT_EXCEPTION_MESSAGE = "Something went wrong.";
16+
private static final String FALLBACK_DEFAULT_MESSAGE = "Something went wrong.";
17+
18+
private final String defaultMessage;
1719

1820
@Nullable
1921
private String message;
2022

2123
@Nullable
2224
private Throwable cause;
2325

24-
protected AbstractFluentExceptionSupport() {}
26+
protected AbstractFluentExceptionSupport() {
27+
this.defaultMessage = FALLBACK_DEFAULT_MESSAGE;
28+
}
2529

26-
public AbstractFluentExceptionSupport<T> message(String message, Object... messageArgs) {
27-
if (messageArgs.length > 0) {
28-
this.message = String.format(message, messageArgs);
29-
} else {
30-
this.message = message;
31-
}
32-
return this;
30+
protected AbstractFluentExceptionSupport(String defaultMessage) {
31+
this.defaultMessage = defaultMessage;
3332
}
3433

35-
public AbstractFluentExceptionSupport<T> message(Supplier<String> messageSupplier) {
36-
this.message = messageSupplier.get();
37-
return this;
34+
// Generic factory methods - to be used by concrete exception classes
35+
public static <T extends Throwable> ExceptionDetailsStage<T> withCause(
36+
Throwable cause, Supplier<AbstractFluentExceptionSupport<T>> builderFactory) {
37+
AbstractFluentExceptionSupport<T> builder = builderFactory.get();
38+
builder.setCause(cause);
39+
return builder;
40+
}
41+
42+
public static <T extends Throwable> ExceptionDetailsStage<T> asRootCause(
43+
Supplier<AbstractFluentExceptionSupport<T>> builderFactory) {
44+
return builderFactory.get();
3845
}
3946

4047
protected Optional<String> message() {
4148
return Optional.ofNullable(message);
4249
}
4350

51+
// @deprecated Use withCause(Throwable) or asRootCause() instead
52+
@Deprecated
4453
public AbstractFluentExceptionSupport<T> cause(Throwable cause) {
4554
this.cause = cause;
4655
return this;
@@ -50,20 +59,35 @@ protected Optional<Throwable> cause() {
5059
return Optional.ofNullable(cause);
5160
}
5261

53-
/**
54-
* Throws exception if condition not satisfied.
55-
* IntelliJ understands that this throws an exception, however, it does understand if the condition is null
56-
* checking. Hence, you may need extra null checks to make IntelliJ happy if your condition is null checking.
57-
* @throws T thrown exception.
58-
*/
62+
@Override
63+
public ExceptionDetailsStage<T> message(String message, Object... messageArgs) {
64+
if (messageArgs.length > 0) {
65+
this.message = String.format(message, messageArgs);
66+
} else {
67+
this.message = message;
68+
}
69+
return this;
70+
}
71+
72+
@Override
73+
public ExceptionDetailsStage<T> message(Supplier<String> messageSupplier) {
74+
this.message = messageSupplier.get();
75+
return this;
76+
}
77+
78+
// Throws exception if condition not satisfied.
79+
// Note: IntelliJ understands that this throws an exception but may need extra null checks for null checking
80+
// conditions.
81+
@Override
5982
public void throwIf(boolean condition) throws T {
6083
if (condition) {
6184
throw exception();
6285
}
6386
}
6487

88+
@Override
6589
public T exception() {
66-
String exceptionMessage = message().orElse(DEFAULT_EXCEPTION_MESSAGE);
90+
String exceptionMessage = message().orElse(defaultMessage);
6791
return cause().map(theCause -> {
6892
if (hasSuppressedExceptions(theCause)) {
6993
String exceptionMessageWithRootCause = appendRootCauseToMessage(exceptionMessage, theCause);
@@ -102,4 +126,9 @@ private static String ensureEndsWithPeriod(String exceptionMessage) {
102126
protected abstract T createExceptionWith(String message);
103127

104128
protected abstract T createExceptionWith(String message, Throwable cause);
129+
130+
// Used by the concrete exception classes
131+
void setCause(Throwable cause) {
132+
this.cause = cause;
133+
}
105134
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.ziro.espresso.fluent.exceptions;
2+
3+
import com.ziro.espresso.javax.annotation.extensions.NonNullByDefault;
4+
import java.util.function.Supplier;
5+
6+
/**
7+
* Second stage of the fluent exception API where message details can be set
8+
* and the exception can be created or thrown.
9+
* This stage is reached after explicitly choosing between withCause() or asRootCause().
10+
*/
11+
@NonNullByDefault
12+
public interface ExceptionDetailsStage<T extends Throwable> {
13+
14+
ExceptionDetailsStage<T> message(String message, Object... messageArgs);
15+
16+
ExceptionDetailsStage<T> message(Supplier<String> messageSupplier);
17+
18+
T exception();
19+
20+
void throwIf(boolean condition) throws T;
21+
}

src/main/java/com/ziro/espresso/fluent/exceptions/SystemUnhandledException.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,27 @@ private SystemUnhandledException(String message, Throwable cause) {
1616
super(message, cause);
1717
}
1818

19+
// Creates a new exception builder that will wrap the given cause.
20+
public static ExceptionDetailsStage<SystemUnhandledException> withCause(Throwable cause) {
21+
return AbstractFluentExceptionSupport.withCause(cause, SystemUnhandledExceptionBuilder::new);
22+
}
23+
24+
// Creates a new exception builder that will be a root cause (no wrapped exception).
25+
public static ExceptionDetailsStage<SystemUnhandledException> asRootCause() {
26+
return AbstractFluentExceptionSupport.asRootCause(SystemUnhandledExceptionBuilder::new);
27+
}
28+
29+
// @deprecated Use withCause(Throwable) or asRootCause() instead
30+
@Deprecated
1931
public static AbstractFluentExceptionSupport<SystemUnhandledException> fluent() {
20-
return new SystemUnhandledException.Fluent();
32+
return new SystemUnhandledExceptionBuilder();
2133
}
2234

23-
private static class Fluent extends AbstractFluentExceptionSupport<SystemUnhandledException> {
35+
private static class SystemUnhandledExceptionBuilder
36+
extends AbstractFluentExceptionSupport<SystemUnhandledException> {
2437

25-
private Fluent() {
26-
message(DEFAULT_MESSAGE);
38+
public SystemUnhandledExceptionBuilder() {
39+
super(DEFAULT_MESSAGE);
2740
}
2841

2942
@Nonnull

src/main/java/com/ziro/espresso/okhttp3/JwtTokenFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static String createAccessToken(String scope, String clientId, String cli
4141
ResponseBody responseBody = Objects.requireNonNull(response.body(), "responseBody should not be null");
4242
String responseAsString = new String(responseBody.bytes());
4343

44-
SystemUnhandledException.fluent()
44+
SystemUnhandledException.asRootCause()
4545
.message(
4646
"Failed to obtain access token from Authorization Server. "
4747
+ "The Authorization Server returned [status_code=%s, response=%s].",

src/main/java/com/ziro/espresso/okhttp3/OAuth2ClientAccessTokens.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ static String getAccessToken(OAuth2ClientAccessTokenRequestParameters oauth2Clie
2323
oauth2ClientAccessTokenRequestParams.clientSecret(),
2424
oauth2ClientAccessTokenRequestParams.tokenUrl()));
2525
} catch (ExecutionException e) {
26-
throw SystemUnhandledException.fluent()
26+
throw SystemUnhandledException.withCause(e.getCause())
2727
.message(
2828
"Something went wrong while trying to get access token for [%s].",
2929
oauth2ClientAccessTokenRequestParams)
30-
.cause(e.getCause())
3130
.exception();
3231
}
3332
}

src/main/java/com/ziro/espresso/okhttp3/OkHttpClientFactory.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ public static SSLSocketFactory buildSocketFactory(X509TrustManager trustManager)
6363
sslContext.init(null, trustAllCerts, new SecureRandom());
6464
return sslContext.getSocketFactory();
6565
} catch (KeyManagementException | NoSuchAlgorithmException e) {
66-
throw SystemUnhandledException.fluent()
66+
throw SystemUnhandledException.withCause(e)
6767
.message("Something went wrong while trying to initialize all-trusting socket factory.")
68-
.cause(e)
6968
.exception();
7069
}
7170
}

src/main/java/com/ziro/espresso/onepasssdk/OnePasswordConnector.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,11 @@ public Properties getSecureNoteAsProperties(String vaultId, String itemId) {
5757
try {
5858
properties.load(in);
5959
} catch (IOException e) {
60-
throw SystemUnhandledException.fluent()
60+
throw SystemUnhandledException.withCause(e)
6161
.message(
6262
"Something went wrong while trying to load secure note as properties using "
6363
+ "[vaultId=%s, itemId=%s].",
6464
vaultId, itemId)
65-
.cause(e)
6665
.exception();
6766
}
6867
return properties;

src/main/java/com/ziro/espresso/properties/PropertiesBuilder.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ private PropertiesBuilder load(URL url) {
6363
try (InputStream in = url.openStream()) {
6464
properties.load(in);
6565
} catch (Exception e) {
66-
throw SystemUnhandledException.fluent()
66+
throw SystemUnhandledException.withCause(e)
6767
.message("Something went wrong while trying to load properties from [url=%s]", url)
68-
.cause(e)
6968
.exception();
7069
}
7170
return this;
@@ -83,7 +82,7 @@ private static URL toUrl(File file) {
8382
try {
8483
return file.toURI().toURL();
8584
} catch (MalformedURLException e) {
86-
throw SystemUnhandledException.fluent()
85+
throw SystemUnhandledException.withCause(e)
8786
.message("Something went wrong while trying to convert file [name=%s] to URL.", file.getName())
8887
.exception();
8988
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.ziro.espresso.fluent.exceptions;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
class SystemUnhandledExceptionTest {
9+
10+
@Test
11+
void whenUsingWithCauseThenExceptionWrapsOriginalCause() {
12+
RuntimeException originalCause = new RuntimeException("Original error");
13+
14+
SystemUnhandledException exception = SystemUnhandledException.withCause(originalCause)
15+
.message("Something went wrong")
16+
.exception();
17+
18+
assertThat(exception.getMessage()).isEqualTo("Something went wrong");
19+
assertThat(exception.getCause()).isEqualTo(originalCause);
20+
}
21+
22+
@Test
23+
void whenUsingAsRootCauseThenExceptionHasNoCause() {
24+
SystemUnhandledException exception = SystemUnhandledException.asRootCause()
25+
.message("Something went wrong")
26+
.exception();
27+
28+
assertThat(exception.getMessage()).isEqualTo("Something went wrong");
29+
assertThat(exception.getCause()).isNull();
30+
}
31+
32+
@Test
33+
void whenUsingWithCauseAndThrowIfThenThrowsExceptionWhenConditionTrue() {
34+
RuntimeException originalCause = new RuntimeException("Original error");
35+
36+
assertThatThrownBy(() -> {
37+
SystemUnhandledException.withCause(originalCause)
38+
.message("Something went wrong")
39+
.throwIf(true);
40+
})
41+
.isInstanceOf(SystemUnhandledException.class)
42+
.hasMessage("Something went wrong")
43+
.hasCause(originalCause);
44+
}
45+
46+
@Test
47+
void whenUsingMessageWithFormatting_thenFormatsCorrectly() {
48+
SystemUnhandledException exception = SystemUnhandledException.asRootCause()
49+
.message("Error processing %s with value %d", "item", 42)
50+
.exception();
51+
52+
assertThat(exception.getMessage()).isEqualTo("Error processing item with value 42");
53+
}
54+
55+
@Test
56+
void whenNoMessageProvidedThenUsesDefaultMessage() {
57+
SystemUnhandledException exception =
58+
SystemUnhandledException.asRootCause().exception();
59+
60+
assertThat(exception.getMessage()).isEqualTo(SystemUnhandledException.DEFAULT_MESSAGE);
61+
}
62+
63+
@Test
64+
void whenNoMessageProvidedWithCauseThenUsesDefaultMessage() {
65+
RuntimeException originalCause = new RuntimeException("Original error");
66+
67+
SystemUnhandledException exception =
68+
SystemUnhandledException.withCause(originalCause).exception();
69+
70+
assertThat(exception.getMessage()).isEqualTo(SystemUnhandledException.DEFAULT_MESSAGE);
71+
assertThat(exception.getCause()).isEqualTo(originalCause);
72+
}
73+
}

0 commit comments

Comments
 (0)