Skip to content

Commit e7b3c89

Browse files
authored
feat(sdk): add linter and fix linter findings (#39)
1 parent 6bbe05b commit e7b3c89

File tree

26 files changed

+567
-252
lines changed

26 files changed

+567
-252
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
- name: Check code format
2828
run: ./gradlew spotlessCheck
2929

30+
- name: Lint
31+
run: make lint
32+
3033
- name: Test
3134
run: make test
3235

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Release (2025-xx-xx)
2+
- `core`: [v0.3.0](core/CHANGELOG.md#v030)
3+
- **Feature:** New exception types for better error handling
4+
- `AuthenticationException`: New exception for authentication-related failures (token generation, refresh, validation)
5+
16
## Release (2025-09-30)
27
- `core`: [v0.2.0](core/CHANGELOG.md#v020)
38
- **Feature:** Support for passing custom OkHttpClient objects

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ fmt:
55
@./gradlew spotlessApply
66

77
lint:
8-
@echo "linting not ready yet"
8+
@./gradlew pmdMain
99

1010
test:
1111
@./gradlew test

build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
id 'signing'
44
id 'idea'
55
id 'eclipse'
6+
id 'pmd'
67

78
id 'com.diffplug.spotless' version '6.21.0'
89

@@ -13,6 +14,7 @@ plugins {
1314

1415
allprojects {
1516
apply plugin: 'com.diffplug.spotless'
17+
apply plugin: 'pmd'
1618

1719
repositories {
1820
mavenCentral()
@@ -64,6 +66,17 @@ allprojects {
6466
endWithNewline()
6567
}
6668
}
69+
70+
pmd {
71+
consoleOutput = true
72+
toolVersion = "7.12.0"
73+
74+
// This tells PMD to use your custom ruleset file.
75+
ruleSetFiles = rootProject.files("config/pmd/pmd-ruleset.xml")
76+
77+
// This is important: it prevents PMD from using its default rules.
78+
ruleSets = []
79+
}
6780
}
6881

6982
def configureMavenCentralPublishing(Project project) {

config/pmd/pmd-ruleset.xml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="Custom Ruleset"
3+
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
6+
7+
<description>
8+
Custom ruleset that excludes generated code from PMD analysis.
9+
</description>
10+
11+
<exclude-pattern>.*/cloud/stackit/sdk/.*/model/.*</exclude-pattern>
12+
<exclude-pattern>.*/cloud/stackit/sdk/.*/api/.*</exclude-pattern>
13+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ApiCallback.java</exclude-pattern>
14+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ApiClient.java</exclude-pattern>
15+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ApiResponse.java</exclude-pattern>
16+
<exclude-pattern>.*/cloud/stackit/sdk/.*/GzipRequestInterceptor.java</exclude-pattern>
17+
<exclude-pattern>.*/cloud/stackit/sdk/.*/JSON.java</exclude-pattern>
18+
<exclude-pattern>.*/cloud/stackit/sdk/.*/Pair.java</exclude-pattern>
19+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ProgressRequestBody.java</exclude-pattern>
20+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ProgressResponseBody.java</exclude-pattern>
21+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ServerConfiguration.java</exclude-pattern>
22+
<exclude-pattern>.*/cloud/stackit/sdk/.*/ServerVariable.java</exclude-pattern>
23+
<exclude-pattern>.*/cloud/stackit/sdk/.*/StringUtil.java</exclude-pattern>
24+
25+
<rule ref="category/java/bestpractices.xml">
26+
<exclude name="UnitTestContainsTooManyAsserts"/>
27+
<exclude name="UnitTestAssertionsShouldIncludeMessage"/>
28+
</rule>
29+
30+
<rule ref="category/java/codestyle.xml">
31+
<exclude name="LocalVariableCouldBeFinal"/>
32+
<exclude name="MethodArgumentCouldBeFinal"/>
33+
<exclude name="AtLeastOneConstructor"/>
34+
<exclude name="LongVariable"/>
35+
<exclude name="OnlyOneReturn"/>
36+
</rule>
37+
38+
<!-- Excluded this rule to allow simple behaviour like parameter.method()
39+
or chained getter calls (transversing dtos) -->
40+
<rule ref="category/java/design.xml">
41+
<exclude name="LawOfDemeter"/>
42+
</rule>
43+
44+
<rule ref="category/java/documentation.xml">
45+
<exclude name="CommentRequired"/>
46+
</rule>
47+
48+
<rule ref="category/java/documentation.xml/CommentSize">
49+
<properties>
50+
<property name="maxLineLength" value="100"/>
51+
<property name="maxLines" value="40"/>
52+
</properties>
53+
</rule>
54+
55+
<rule ref="category/java/errorprone.xml">
56+
<exclude name="AvoidFieldNameMatchingMethodName"/>
57+
</rule>
58+
59+
<rule ref="category/java/multithreading.xml">
60+
</rule>
61+
62+
<rule ref="category/java/performance.xml">
63+
</rule>
64+
65+
<rule ref="category/java/security.xml">
66+
</rule>
67+
68+
</ruleset>

core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v0.3.0
2+
- **Feature:** New exception types for better error handling
3+
- `AuthenticationException`: New exception for authentication-related failures (token generation, refresh, validation)
4+
15
## v0.2.0
26
- **Feature:** Support for passing custom OkHttpClient objects
37
- `KeyFlowAuthenticator`: Add new constructors with an `OkHttpClientParam`

core/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.3.0

core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import cloud.stackit.sdk.core.config.CoreConfiguration;
55
import cloud.stackit.sdk.core.config.EnvironmentVariables;
66
import cloud.stackit.sdk.core.exception.ApiException;
7+
import cloud.stackit.sdk.core.exception.AuthenticationException;
78
import cloud.stackit.sdk.core.model.ServiceAccountKey;
89
import cloud.stackit.sdk.core.utils.Utils;
910
import com.auth0.jwt.JWT;
@@ -19,14 +20,16 @@
1920
import java.security.interfaces.RSAPrivateKey;
2021
import java.security.spec.InvalidKeySpecException;
2122
import java.util.Date;
22-
import java.util.HashMap;
2323
import java.util.Map;
2424
import java.util.UUID;
25+
import java.util.concurrent.ConcurrentHashMap;
2526
import java.util.concurrent.TimeUnit;
2627
import okhttp3.*;
2728
import org.jetbrains.annotations.NotNull;
2829

29-
/* KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */
30+
/*
31+
* KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key.
32+
*/
3033
public class KeyFlowAuthenticator implements Authenticator {
3134
private static final String REFRESH_TOKEN = "refresh_token";
3235
private static final String ASSERTION = "assertion";
@@ -44,6 +47,8 @@ public class KeyFlowAuthenticator implements Authenticator {
4447
private final String tokenUrl;
4548
private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY;
4649

50+
private final Object tokenRefreshMonitor = new Object();
51+
4752
/**
4853
* Creates the initial service account and refreshes expired access token.
4954
*
@@ -128,7 +133,7 @@ public Request authenticate(Route route, @NotNull Response response) throws IOEx
128133
try {
129134
accessToken = getAccessToken();
130135
} catch (ApiException | InvalidKeySpecException e) {
131-
throw new RuntimeException(e);
136+
throw new AuthenticationException("Failed to obtain access token", e);
132137
}
133138

134139
// Return a new request with the refreshed token
@@ -140,19 +145,19 @@ public Request authenticate(Route route, @NotNull Response response) throws IOEx
140145

141146
protected static class KeyFlowTokenResponse {
142147
@SerializedName("access_token")
143-
private String accessToken;
148+
private final String accessToken;
144149

145150
@SerializedName("refresh_token")
146-
private String refreshToken;
151+
private final String refreshToken;
147152

148153
@SerializedName("expires_in")
149154
private long expiresIn;
150155

151156
@SerializedName("scope")
152-
private String scope;
157+
private final String scope;
153158

154159
@SerializedName("token_type")
155-
private String tokenType;
160+
private final String tokenType;
156161

157162
public KeyFlowTokenResponse(
158163
String accessToken,
@@ -184,14 +189,16 @@ protected String getAccessToken() {
184189
* @throws IOException request for new access token failed
185190
* @throws ApiException response for new access token with bad status code
186191
*/
187-
public synchronized String getAccessToken()
188-
throws IOException, ApiException, InvalidKeySpecException {
189-
if (token == null) {
190-
createAccessToken();
191-
} else if (token.isExpired()) {
192-
createAccessTokenWithRefreshToken();
192+
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
193+
public String getAccessToken() throws IOException, ApiException, InvalidKeySpecException {
194+
synchronized (tokenRefreshMonitor) {
195+
if (token == null) {
196+
createAccessToken();
197+
} else if (token.isExpired()) {
198+
createAccessTokenWithRefreshToken();
199+
}
200+
return token.getAccessToken();
193201
}
194-
return token.getAccessToken();
195202
}
196203

197204
/**
@@ -202,20 +209,23 @@ public synchronized String getAccessToken()
202209
* @throws ApiException response for new access token with bad status code
203210
* @throws JsonSyntaxException parsing of the created access token failed
204211
*/
205-
protected void createAccessToken()
206-
throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException {
207-
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
208-
String assertion;
209-
try {
210-
assertion = generateSelfSignedJWT();
211-
} catch (NoSuchAlgorithmException e) {
212-
throw new RuntimeException(
213-
"could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues",
214-
e);
212+
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
213+
protected void createAccessToken() throws InvalidKeySpecException, IOException, ApiException {
214+
synchronized (tokenRefreshMonitor) {
215+
String assertion;
216+
try {
217+
assertion = generateSelfSignedJWT();
218+
} catch (NoSuchAlgorithmException e) {
219+
throw new AuthenticationException(
220+
"could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues",
221+
e);
222+
}
223+
224+
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
225+
try (Response response = requestToken(grant, assertion).execute()) {
226+
parseTokenResponse(response);
227+
}
215228
}
216-
Response response = requestToken(grant, assertion).execute();
217-
parseTokenResponse(response);
218-
response.close();
219229
}
220230

221231
/**
@@ -225,16 +235,24 @@ protected void createAccessToken()
225235
* @throws ApiException response for new access token with bad status code
226236
* @throws JsonSyntaxException can not parse new access token
227237
*/
228-
protected synchronized void createAccessTokenWithRefreshToken()
229-
throws IOException, JsonSyntaxException, ApiException {
230-
String refreshToken = token.refreshToken;
231-
Response response = requestToken(REFRESH_TOKEN, refreshToken).execute();
232-
parseTokenResponse(response);
233-
response.close();
238+
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
239+
protected void createAccessTokenWithRefreshToken() throws IOException, ApiException {
240+
synchronized (tokenRefreshMonitor) {
241+
String refreshToken = token.refreshToken;
242+
try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) {
243+
parseTokenResponse(response);
244+
}
245+
}
234246
}
235247

236-
private synchronized void parseTokenResponse(Response response)
237-
throws ApiException, JsonSyntaxException, IOException {
248+
/**
249+
* Parses the token response from the server
250+
*
251+
* @param response HTTP response containing the token
252+
* @throws ApiException if the response has a bad status code
253+
* @throws JsonSyntaxException if the response body cannot be parsed
254+
*/
255+
private void parseTokenResponse(Response response) throws ApiException {
238256
if (response.code() != HttpURLConnection.HTTP_OK) {
239257
String body = null;
240258
if (response.body() != null) {
@@ -256,10 +274,10 @@ private synchronized void parseTokenResponse(Response response)
256274
response.body().close();
257275
}
258276

259-
private Call requestToken(String grant, String assertionValue) throws IOException {
277+
private Call requestToken(String grant, String assertionValue) {
260278
FormBody.Builder bodyBuilder = new FormBody.Builder();
261279
bodyBuilder.addEncoded("grant_type", grant);
262-
String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION;
280+
String assertionKey = REFRESH_TOKEN.equals(grant) ? REFRESH_TOKEN : ASSERTION;
263281
bodyBuilder.addEncoded(assertionKey, assertionValue);
264282
FormBody body = bodyBuilder.build();
265283

@@ -289,7 +307,7 @@ private String generateSelfSignedJWT()
289307
prvKey = saKey.getCredentials().getPrivateKeyParsed();
290308
Algorithm algorithm = Algorithm.RSA512(prvKey);
291309

292-
Map<String, Object> jwtHeader = new HashMap<>();
310+
Map<String, Object> jwtHeader = new ConcurrentHashMap<>();
293311
jwtHeader.put("kid", saKey.getCredentials().getKid());
294312

295313
return JWT.create()

core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cloud.stackit.sdk.core;
22

33
import cloud.stackit.sdk.core.exception.ApiException;
4+
import cloud.stackit.sdk.core.exception.AuthenticationException;
45
import java.io.IOException;
56
import java.security.spec.InvalidKeySpecException;
67
import okhttp3.Interceptor;
@@ -37,7 +38,8 @@ public Response intercept(Chain chain) throws IOException {
3738
} catch (InvalidKeySpecException | ApiException e) {
3839
// try-catch required, because ApiException can not be thrown in the implementation
3940
// of Interceptor.intercept(Chain chain)
40-
throw new RuntimeException(e);
41+
throw new AuthenticationException(
42+
"Failed to obtain access token for request authentication", e);
4143
}
4244

4345
Request authenticatedRequest =

0 commit comments

Comments
 (0)