Skip to content

Commit c44c16e

Browse files
authored
extend CI to run tests with JDK 21 (#1408)
This was previously blocked by our Gradle version being too old. Now that we can run the tests with JDK 21 it shows though, that the `Annotation` `toString()` behavior has changed again. Unfortunately, this always either causes complexity on the production or test side. If we want to test the `AnnotationProxy.toString()` behavior, then either we have to add a workaround to the test code, or we have to adjust the production code to produce the equivalent `toString()` value. I opted for the latter in hopes that they will finally stop changing the `toString()` style every couple of versions.
2 parents 87f6931 + 1102ae9 commit c44c16e

File tree

17 files changed

+348
-200
lines changed

17 files changed

+348
-200
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
pull_request:
99

1010
env:
11-
build_java_version: 17
11+
build_java_version: 21
1212

1313
jobs:
1414
build:
@@ -47,6 +47,7 @@ jobs:
4747
- 8
4848
- 11
4949
- 17
50+
- 21
5051
runs-on: ${{ matrix.os }}
5152
steps:
5253
- name: Checkout
@@ -96,6 +97,7 @@ jobs:
9697
- 8
9798
- 11
9899
- 17
100+
- 21
99101
runs-on: ${{ matrix.os }}
100102
steps:
101103
- name: Checkout

archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
1515
*/
1616
package com.tngtech.archunit.core.domain;
1717

18-
import com.tngtech.archunit.Internal;
1918
import com.tngtech.archunit.core.InitialConfiguration;
2019
import com.tngtech.archunit.core.PluginLoader;
2120

2221
/**
2322
* Resolved via {@link PluginLoader}
2423
*/
2524
@SuppressWarnings("unused")
26-
@Internal
27-
public class Java9DomainPlugin implements DomainPlugin {
25+
class Java9DomainPlugin implements DomainPlugin {
2826
@Override
29-
public void plugInAnnotationPropertiesFormatter(InitialConfiguration<AnnotationPropertiesFormatter> propertiesFormatter) {
30-
propertiesFormatter.set(AnnotationPropertiesFormatter.configure()
31-
.formattingArraysWithCurlyBrackets()
32-
.formattingTypesAsClassNames()
33-
.quotingStrings()
34-
.build());
27+
public void plugInAnnotationFormatter(InitialConfiguration<AnnotationFormatter> propertiesFormatter) {
28+
propertiesFormatter.set(
29+
AnnotationFormatter.formatAnnotationType(JavaClass::getName)
30+
.formatProperties(config -> config
31+
.formattingArraysWithCurlyBrackets()
32+
.formattingTypesAsClassNames()
33+
.quotingStrings()
34+
));
3535
}
3636
}

archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.tngtech.archunit.core;
1717

18+
import java.lang.reflect.Constructor;
1819
import java.util.HashMap;
1920
import java.util.List;
2021
import java.util.Map;
@@ -83,7 +84,9 @@ private T create(String className) {
8384
try {
8485
Class<?> clazz = Class.forName(className);
8586
checkCompatibility(className, clazz);
86-
return (T) clazz.getConstructor().newInstance();
87+
Constructor<?> constructor = clazz.getDeclaredConstructor();
88+
constructor.setAccessible(true);
89+
return (T) constructor.newInstance();
8790
} catch (Exception e) {
8891
throw new PluginLoadingFailedException(e, "Couldn't load plugin of type %s", className);
8992
}
@@ -136,7 +139,8 @@ public Creator<T> load(String pluginClassName) {
136139
public enum JavaVersion {
137140

138141
JAVA_9(9),
139-
JAVA_14(14);
142+
JAVA_14(14),
143+
JAVA_21(21);
140144

141145
private final int releaseVersion;
142146

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2014-2025 TNG Technology Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.tngtech.archunit.core.domain;
17+
18+
import java.lang.reflect.Array;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.function.Consumer;
22+
import java.util.function.Function;
23+
import java.util.stream.IntStream;
24+
25+
import com.google.common.base.Joiner;
26+
27+
import static com.google.common.base.Preconditions.checkNotNull;
28+
import static java.util.function.Function.identity;
29+
import static java.util.stream.Collectors.joining;
30+
import static java.util.stream.Collectors.toList;
31+
32+
class AnnotationFormatter {
33+
private final Function<JavaClass, String> annotationTypeFormatter;
34+
private final AnnotationPropertiesFormatter propertiesFormatter;
35+
36+
AnnotationFormatter(Function<JavaClass, String> annotationTypeFormatter, AnnotationPropertiesFormatter propertiesFormatter) {
37+
this.annotationTypeFormatter = annotationTypeFormatter;
38+
this.propertiesFormatter = propertiesFormatter;
39+
}
40+
41+
String format(JavaClass annotationType, Map<String, Object> annotationProperties) {
42+
return String.format("@%s(%s)", annotationTypeFormatter.apply(annotationType), propertiesFormatter.formatProperties(annotationProperties));
43+
}
44+
45+
static Builder formatAnnotationType(Function<JavaClass, String> annotationTypeFormatter) {
46+
return new Builder(annotationTypeFormatter);
47+
}
48+
49+
static class Builder {
50+
private final Function<JavaClass, String> annotationTypeFormatter;
51+
52+
private Builder(Function<JavaClass, String> annotationTypeFormatter) {
53+
this.annotationTypeFormatter = annotationTypeFormatter;
54+
}
55+
56+
AnnotationFormatter formatProperties(Consumer<AnnotationPropertiesFormatter.Builder> config) {
57+
AnnotationPropertiesFormatter.Builder propertiesFormatterBuilder = AnnotationPropertiesFormatter.configure();
58+
config.accept(propertiesFormatterBuilder);
59+
return new AnnotationFormatter(annotationTypeFormatter, propertiesFormatterBuilder.build());
60+
}
61+
}
62+
63+
static class AnnotationPropertiesFormatter {
64+
private final Function<List<String>, String> arrayFormatter;
65+
private final Function<Class<?>, String> typeFormatter;
66+
private final Function<String, String> stringFormatter;
67+
private final boolean omitOptionalIdentifierForSingleElementAnnotations;
68+
69+
private AnnotationPropertiesFormatter(Builder builder) {
70+
this.arrayFormatter = checkNotNull(builder.arrayFormatter);
71+
this.typeFormatter = checkNotNull(builder.typeFormatter);
72+
this.stringFormatter = checkNotNull(builder.stringFormatter);
73+
this.omitOptionalIdentifierForSingleElementAnnotations = builder.omitOptionalIdentifierForSingleElementAnnotations;
74+
}
75+
76+
String formatProperties(Map<String, Object> properties) {
77+
// see Builder#omitOptionalIdentifierForSingleElementAnnotations() for documentation
78+
if (properties.size() == 1 && properties.containsKey("value") && omitOptionalIdentifierForSingleElementAnnotations) {
79+
return formatValue(properties.get("value"));
80+
}
81+
82+
return properties.entrySet().stream()
83+
.map(entry -> entry.getKey() + "=" + formatValue(entry.getValue()))
84+
.collect(joining(", "));
85+
}
86+
87+
String formatValue(Object input) {
88+
if (input instanceof Class<?>) {
89+
return typeFormatter.apply((Class<?>) input);
90+
}
91+
if (input instanceof String) {
92+
return stringFormatter.apply((String) input);
93+
}
94+
if (!input.getClass().isArray()) {
95+
return String.valueOf(input);
96+
}
97+
98+
List<String> elemToString = IntStream.range(0, Array.getLength(input))
99+
.mapToObj(i -> formatValue(Array.get(input, i)))
100+
.collect(toList());
101+
return arrayFormatter.apply(elemToString);
102+
}
103+
104+
static Builder configure() {
105+
return new Builder();
106+
}
107+
108+
static class Builder {
109+
private Function<List<String>, String> arrayFormatter;
110+
private Function<Class<?>, String> typeFormatter;
111+
private Function<String, String> stringFormatter = identity();
112+
private boolean omitOptionalIdentifierForSingleElementAnnotations = false;
113+
114+
Builder formattingArraysWithSquareBrackets() {
115+
arrayFormatter = input -> "[" + Joiner.on(", ").join(input) + "]";
116+
return this;
117+
}
118+
119+
Builder formattingArraysWithCurlyBrackets() {
120+
arrayFormatter = input -> "{" + Joiner.on(", ").join(input) + "}";
121+
return this;
122+
}
123+
124+
Builder formattingTypesToString() {
125+
typeFormatter = String::valueOf;
126+
return this;
127+
}
128+
129+
Builder formattingTypesAsClassNames() {
130+
typeFormatter = input -> input.getName() + ".class";
131+
return this;
132+
}
133+
134+
Builder quotingStrings() {
135+
stringFormatter = input -> "\"" + input + "\"";
136+
return this;
137+
}
138+
139+
/**
140+
* Configures that the identifier is omitted if the annotation is a
141+
* <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-9.html#jls-9.7.3">single-element annotation</a>
142+
* and the identifier of the only element is "value".
143+
*
144+
* <ul><li>Example with this configuration: {@code @Copyright("2020 Acme Corporation")}</li>
145+
* <li>Example without this configuration: {@code @Copyright(value="2020 Acme Corporation")}</li></ul>
146+
*/
147+
Builder omitOptionalIdentifierForSingleElementAnnotations() {
148+
omitOptionalIdentifierForSingleElementAnnotations = true;
149+
return this;
150+
}
151+
152+
AnnotationPropertiesFormatter build() {
153+
return new AnnotationPropertiesFormatter(this);
154+
}
155+
}
156+
}
157+
}

archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java

Lines changed: 0 additions & 124 deletions
This file was deleted.

archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@
3737

3838
@MayResolveTypesViaReflection(reason = "We depend on the classpath, if we proxy an annotation type")
3939
class AnnotationProxy {
40-
private static final InitialConfiguration<AnnotationPropertiesFormatter> propertiesFormatter = new InitialConfiguration<>();
40+
private static final InitialConfiguration<AnnotationFormatter> annotationFormatter = new InitialConfiguration<>();
4141

4242
static {
43-
DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationPropertiesFormatter(propertiesFormatter);
43+
DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationFormatter(annotationFormatter);
4444
}
4545

4646
public static <A extends Annotation> A of(Class<A> annotationType, JavaAnnotation<?> toProxy) {
@@ -277,12 +277,7 @@ private ToStringHandler(Class<?> annotationType, JavaAnnotation<?> toProxy, Conv
277277

278278
@Override
279279
public Object handle(Object proxy, Method method, Object[] args) {
280-
return String.format("@%s(%s)", toProxy.getRawType().getName(), propertiesString());
281-
}
282-
283-
private String propertiesString() {
284-
Map<String, Object> unwrappedProperties = unwrapProxiedProperties();
285-
return propertiesFormatter.get().formatProperties(unwrappedProperties);
280+
return annotationFormatter.get().format(toProxy.getRawType(), unwrapProxiedProperties());
286281
}
287282

288283
private Map<String, Object> unwrapProxiedProperties() {

0 commit comments

Comments
 (0)