Skip to content

Commit 3bf06b4

Browse files
sync with smacs
* update classes to match smacs repo * Bump version to 1.1.0 * Apply spotless * Change SynchronousCallAdapterFactory to SyncResponseAdapterFactory and match it to smacs implementation * spotless * restore SynchronousCallAdapterFactory * appease the gods * fixer --------- Co-authored-by: Arturo Rotondo <3749826+arotondo@users.noreply.github.com>
1 parent 4f9f6e2 commit 3bf06b4

11 files changed

Lines changed: 214 additions & 67 deletions

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dependencies {
4141
implementation 'com.google.guava:guava:32.1.1-jre'
4242
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
4343
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
44+
implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'
4445
implementation 'org.slf4j:slf4j-api:2.0.12'
4546

4647
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.ziro.espresso.fluent.exceptions;
22

3+
import com.google.common.base.Strings;
4+
import com.google.common.base.Throwables;
5+
import com.ziro.espresso.javax.annotation.extensions.NonNullByDefault;
6+
import jakarta.annotation.Nullable;
37
import java.util.Optional;
4-
import javax.annotation.Nullable;
8+
import java.util.function.Supplier;
59

610
/**
711
* Provides fluent support to any type of throwable.
812
*/
13+
@NonNullByDefault
914
public abstract class AbstractFluentExceptionSupport<T extends Throwable> {
1015

1116
private static final String DEFAULT_EXCEPTION_MESSAGE = "Something went wrong.";
@@ -27,6 +32,11 @@ public AbstractFluentExceptionSupport<T> message(String message, Object... messa
2732
return this;
2833
}
2934

35+
public AbstractFluentExceptionSupport<T> message(Supplier<String> messageSupplier) {
36+
this.message = messageSupplier.get();
37+
return this;
38+
}
39+
3040
protected Optional<String> message() {
3141
return Optional.ofNullable(message);
3242
}
@@ -53,12 +63,42 @@ public void throwIf(boolean condition) throws T {
5363
}
5464

5565
public T exception() {
56-
String currentMessage = message().orElse(DEFAULT_EXCEPTION_MESSAGE);
57-
return cause().map(theCause -> createExceptionWith(currentMessage, theCause))
58-
.orElse(createExceptionWith(currentMessage));
66+
String exceptionMessage = message().orElse(DEFAULT_EXCEPTION_MESSAGE);
67+
return cause().map(theCause -> {
68+
if (hasSuppressedExceptions(theCause)) {
69+
String exceptionMessageWithRootCause = appendRootCauseToMessage(exceptionMessage, theCause);
70+
return createExceptionWith(exceptionMessageWithRootCause, theCause);
71+
}
72+
return createExceptionWith(exceptionMessage, theCause);
73+
})
74+
.orElse(createExceptionWith(exceptionMessage));
75+
}
76+
77+
private static boolean hasSuppressedExceptions(Throwable theCause) {
78+
Throwable[] suppressed = theCause.getSuppressed();
79+
// Although suppressed should never be null,
80+
// being null safe here saves us some headaches with unit tests
81+
// where we've had to mock exceptions for wtv
82+
// (there aren't many, but I think it will be less confusing this way).
83+
return suppressed != null && suppressed.length > 0;
84+
}
85+
86+
private static String appendRootCauseToMessage(String exceptionMessage, Throwable theCause) {
87+
Throwable rootCause = Throwables.getRootCause(theCause);
88+
String rootCauseExceptionMessage = rootCause.getMessage();
89+
if (Strings.isNullOrEmpty(rootCauseExceptionMessage)) {
90+
return exceptionMessage;
91+
} else {
92+
return "%s Root cause: %s"
93+
.formatted(ensureEndsWithPeriod(exceptionMessage), ensureEndsWithPeriod(rootCauseExceptionMessage));
94+
}
95+
}
96+
97+
private static String ensureEndsWithPeriod(String exceptionMessage) {
98+
return exceptionMessage.endsWith(".") ? exceptionMessage : exceptionMessage + ".";
5999
}
60100

61-
// The following abstract methods is the reason why we had to go abstract on this
101+
// The following abstract methods are the reason why we had to go abstract on this
62102
protected abstract T createExceptionWith(String message);
63103

64104
protected abstract T createExceptionWith(String message, Throwable cause);

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.ziro.espresso.fluent.exceptions;
22

3-
import javax.annotation.Nonnull;
3+
import com.ziro.espresso.javax.annotation.extensions.NonNullByDefault;
4+
import jakarta.annotation.Nonnull;
45

6+
@NonNullByDefault
57
public class SystemUnhandledException extends RuntimeException {
68

7-
public static final String DEFAULT_MESSAGE = "Something went wrong and caused an unhandled exception.";
9+
public static final String DEFAULT_MESSAGE = "ZIRO encountered an error it could not recover from.";
810

911
private SystemUnhandledException(String message) {
1012
super(message);
@@ -15,7 +17,7 @@ private SystemUnhandledException(String message, Throwable cause) {
1517
}
1618

1719
public static AbstractFluentExceptionSupport<SystemUnhandledException> fluent() {
18-
return new Fluent();
20+
return new SystemUnhandledException.Fluent();
1921
}
2022

2123
private static class Fluent extends AbstractFluentExceptionSupport<SystemUnhandledException> {

src/main/java/com/ziro/espresso/formatters/MaxLengthFormatter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.ziro.espresso.formatters;
22

33
import com.google.common.base.Preconditions;
4+
import com.ziro.espresso.javax.annotation.extensions.NonNullByDefault;
45
import org.slf4j.Logger;
56
import org.slf4j.LoggerFactory;
67

8+
@NonNullByDefault
79
public class MaxLengthFormatter {
810

911
private static final Logger LOGGER = LoggerFactory.getLogger(MaxLengthFormatter.class);

src/main/java/com/ziro/espresso/javax/annotation/extensions/NonNullByDefault.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.ziro.espresso.javax.annotation.extensions;
22

3+
import jakarta.annotation.Nonnull;
34
import java.lang.annotation.Documented;
45
import java.lang.annotation.ElementType;
56
import java.lang.annotation.Retention;
67
import java.lang.annotation.RetentionPolicy;
7-
import javax.annotation.Nonnull;
88
import javax.annotation.meta.TypeQualifierDefault;
99

1010
@Documented
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.ziro.espresso.okhttp3;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonObject;
5+
import com.google.gson.JsonParser;
6+
import com.ziro.espresso.fluent.exceptions.SystemUnhandledException;
7+
import java.io.IOException;
8+
import java.util.Objects;
9+
import okhttp3.Call;
10+
import okhttp3.FormBody;
11+
import okhttp3.OkHttpClient;
12+
import okhttp3.Request;
13+
import okhttp3.RequestBody;
14+
import okhttp3.Response;
15+
import okhttp3.ResponseBody;
16+
17+
public class JwtTokenFactory {
18+
19+
private static final int SUCCESS_STATUS_CODE = 200;
20+
21+
public static String createAccessToken(String scope, String clientId, String clientSecret, String tokenUrl)
22+
throws IOException {
23+
OkHttpClient client = new OkHttpClient().newBuilder().build();
24+
25+
RequestBody requestBody = new FormBody.Builder()
26+
.addEncoded("grant_type", "client_credentials")
27+
.addEncoded("scope", scope)
28+
.addEncoded("client_id", clientId)
29+
.addEncoded("client_secret", clientSecret)
30+
.build();
31+
32+
Request request = new Request.Builder()
33+
.url(tokenUrl)
34+
.addHeader("Content-Type", "application/x-www-form-urlencoded")
35+
.addHeader("Accept", "application/json")
36+
.post(requestBody)
37+
.build();
38+
39+
Call call = client.newCall(request);
40+
try (Response response = call.execute()) {
41+
ResponseBody responseBody = Objects.requireNonNull(response.body(), "responseBody should not be null");
42+
String responseAsString = new String(responseBody.bytes());
43+
44+
SystemUnhandledException.fluent()
45+
.message(
46+
"Failed to obtain access token from Authorization Server. "
47+
+ "The Authorization Server returned [status_code=%s, response=%s].",
48+
response.code(), responseAsString)
49+
.throwIf(response.code() != SUCCESS_STATUS_CODE);
50+
51+
JsonParser jsonParser = new JsonParser();
52+
JsonElement jwtJsonElement = jsonParser.parse(responseAsString);
53+
JsonObject jwtJsonObject = jwtJsonElement.getAsJsonObject();
54+
return jwtJsonObject.get("access_token").getAsString();
55+
}
56+
}
57+
}

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

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,26 @@
22

33
import com.google.common.cache.Cache;
44
import com.google.common.cache.CacheBuilder;
5-
import com.google.gson.JsonElement;
6-
import com.google.gson.JsonObject;
7-
import com.google.gson.JsonParser;
85
import com.ziro.espresso.fluent.exceptions.SystemUnhandledException;
9-
import java.io.IOException;
106
import java.time.Duration;
11-
import java.util.Objects;
127
import java.util.concurrent.ExecutionException;
13-
import okhttp3.Call;
14-
import okhttp3.FormBody;
15-
import okhttp3.OkHttpClient;
16-
import okhttp3.Request;
17-
import okhttp3.RequestBody;
18-
import okhttp3.Response;
19-
import okhttp3.ResponseBody;
208

21-
class OAuth2ClientAccessTokens {
9+
public class OAuth2ClientAccessTokens {
2210

2311
private static final Cache<String, String> ACCESS_TOKENS_CACHE =
2412
CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(60)).build();
25-
private static final int SUCCESS_STATUS_CODE = 200;
2613

2714
private OAuth2ClientAccessTokens() {}
2815

2916
static String getAccessToken(OAuth2ClientAccessTokenRequestParameters oauth2ClientAccessTokenRequestParams) {
3017
try {
3118
return ACCESS_TOKENS_CACHE.get(
3219
oauth2ClientAccessTokenRequestParams.toString(),
33-
() -> accessTokensCacheLoader(oauth2ClientAccessTokenRequestParams));
20+
() -> JwtTokenFactory.createAccessToken(
21+
oauth2ClientAccessTokenRequestParams.scope(),
22+
oauth2ClientAccessTokenRequestParams.clientId(),
23+
oauth2ClientAccessTokenRequestParams.clientSecret(),
24+
oauth2ClientAccessTokenRequestParams.tokenUrl()));
3425
} catch (ExecutionException e) {
3526
throw SystemUnhandledException.fluent()
3627
.message(
@@ -40,40 +31,4 @@ static String getAccessToken(OAuth2ClientAccessTokenRequestParameters oauth2Clie
4031
.exception();
4132
}
4233
}
43-
44-
private static String accessTokensCacheLoader(
45-
OAuth2ClientAccessTokenRequestParameters oauth2ClientAccessTokenRequestParams) throws IOException {
46-
OkHttpClient client = new OkHttpClient().newBuilder().build();
47-
48-
RequestBody requestBody = new FormBody.Builder()
49-
.addEncoded("grant_type", "client_credentials")
50-
.addEncoded("scope", oauth2ClientAccessTokenRequestParams.scope())
51-
.addEncoded("client_id", oauth2ClientAccessTokenRequestParams.clientId())
52-
.addEncoded("client_secret", oauth2ClientAccessTokenRequestParams.clientSecret())
53-
.build();
54-
55-
Request request = new Request.Builder()
56-
.url(oauth2ClientAccessTokenRequestParams.tokenUrl())
57-
.addHeader("Content-Type", "application/x-www-form-urlencoded")
58-
.addHeader("Accept", "application/json")
59-
.post(requestBody)
60-
.build();
61-
62-
Call call = client.newCall(request);
63-
try (Response response = call.execute()) {
64-
ResponseBody responseBody = Objects.requireNonNull(response.body(), "responseBody should not be null");
65-
String responseAsString = new String(responseBody.bytes());
66-
67-
SystemUnhandledException.fluent()
68-
.message(
69-
"Failed to obtain access token from Authorization Server. "
70-
+ "The Authorization Server returned [status_code=%s, response=%s].",
71-
response.code(), responseAsString)
72-
.throwIf(response.code() != SUCCESS_STATUS_CODE);
73-
74-
JsonElement jwtJsonElement = JsonParser.parseString(responseAsString);
75-
JsonObject jwtJsonObject = jwtJsonElement.getAsJsonObject();
76-
return jwtJsonObject.get("access_token").getAsString();
77-
}
78-
}
7934
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public static SSLSocketFactory buildSocketFactory(X509TrustManager trustManager)
7070
}
7171
}
7272

73+
public static X509TrustManager buildX509TrustManager() {
74+
return new NaiveX509TrustManager();
75+
}
76+
7377
public static X509TrustManager createNaiveX509TrustManager() {
7478
return new NaiveX509TrustManager();
7579
}
@@ -78,12 +82,14 @@ private static class NaiveX509TrustManager implements X509TrustManager {
7882

7983
@Override
8084
public void checkClientTrusted(X509Certificate[] chain, String authType) {
81-
// Do nothing - trust all clients
85+
// We trust any client/server for now since we deploy in our own OS,
86+
// so that there is no need to worry about incoming or outcome transactions
8287
}
8388

8489
@Override
8590
public void checkServerTrusted(X509Certificate[] chain, String authType) {
86-
// Do nothing - trust all servers
91+
// We trust any client/server for now since we deploy in our own OS,
92+
// so that there is no need to worry about incoming or outcome transactions
8793
}
8894

8995
@Override
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.ziro.espresso.okhttp3;
2+
3+
import java.io.IOException;
4+
import java.lang.annotation.Annotation;
5+
import java.lang.reflect.ParameterizedType;
6+
import java.lang.reflect.Type;
7+
import javax.annotation.ParametersAreNonnullByDefault;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import retrofit2.Call;
11+
import retrofit2.CallAdapter;
12+
import retrofit2.Response;
13+
import retrofit2.Retrofit;
14+
15+
@ParametersAreNonnullByDefault
16+
public class SyncResponseAdapterFactory<R, T> extends CallAdapter.Factory {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(SyncResponseAdapterFactory.class);
18+
19+
@Override
20+
public CallAdapter<R, T> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
21+
22+
// public Call<FooJson> foo();
23+
// retrofit2.DefaultCallAdapterFactory handles this
24+
if (getRawType(returnType).equals(Call.class)) {
25+
return null;
26+
}
27+
28+
if (getRawType(returnType).equals(Response.class)) {
29+
//noinspection AnonymousInnerClass
30+
return new CallAdapter<>() {
31+
@Override
32+
public Type responseType() {
33+
return getParameterUpperBound(0, (ParameterizedType) returnType);
34+
}
35+
36+
@Override
37+
public T adapt(Call<R> call) {
38+
Response<R> response;
39+
try {
40+
response = call.execute();
41+
} catch (IOException e) {
42+
throw new RuntimeException(e);
43+
}
44+
//noinspection unchecked
45+
return (T) response;
46+
}
47+
};
48+
}
49+
50+
//noinspection AnonymousInnerClass
51+
return new CallAdapter<>() {
52+
@Override
53+
public Type responseType() {
54+
return returnType;
55+
}
56+
57+
@Override
58+
public T adapt(Call<R> call) {
59+
Response<R> response;
60+
try {
61+
response = call.execute();
62+
} catch (IOException e) {
63+
throw new RuntimeException(e);
64+
}
65+
66+
if (!response.isSuccessful()) {
67+
String errorBody;
68+
try {
69+
//noinspection resource,ConstantConditions
70+
errorBody = new String(response.errorBody().bytes());
71+
} catch (IOException e) {
72+
throw new RuntimeException(e);
73+
}
74+
String errorMessage = String.format("Unsuccessful status %s: %s", response.code(), errorBody);
75+
LOGGER.debug(errorMessage);
76+
// This doesn't throw a specific exception type since the caller can return a Response<FooJson>
77+
// type and inspect the response directly if they are interested in the status code.
78+
throw new RuntimeException(errorMessage);
79+
}
80+
81+
//noinspection unchecked
82+
return (T) response.body();
83+
}
84+
};
85+
}
86+
}

0 commit comments

Comments
 (0)