Skip to content

Commit 9429772

Browse files
authored
fix nullability customizer (#47)
* fix nullability customizer * fix tests and add annotation check by simple name string * fix and cleanup tests
1 parent a8c040f commit 9429772

7 files changed

Lines changed: 365 additions & 146 deletions

File tree

pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<modelVersion>4.0.0</modelVersion>
45

56
<parent>
@@ -26,7 +27,7 @@
2627
<dependency>
2728
<groupId>it.aboutbits</groupId>
2829
<artifactId>archunit-toolbox</artifactId>
29-
<version>1.0.0-RC1</version>
30+
<version>1.1.0</version>
3031
</dependency>
3132
</dependencies>
3233
</dependencyManagement>

src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import org.jspecify.annotations.NullMarked;
66
import org.springdoc.core.customizers.OpenApiCustomizer;
77

8+
import java.lang.annotation.Annotation;
9+
import java.lang.reflect.AnnotatedType;
810
import java.util.ArrayList;
911
import java.util.Map;
1012

1113
@NullMarked
1214
public class NullableCustomizer implements OpenApiCustomizer {
13-
public static final String NULLABLE_MARKER = "NULLABLE";
14-
1515
@Override
1616
@SuppressWarnings("unchecked")
1717
public void customise(OpenAPI openApi) {
@@ -23,57 +23,130 @@ public void customise(OpenAPI openApi) {
2323
var requiredProperties = new ArrayList<String>();
2424
if (((Schema<?>) schema).getProperties() != null) {
2525
var properties = ((Schema<?>) schema).getProperties();
26-
processProperties(properties, requiredProperties);
26+
processProperties(schema.getName(), properties, requiredProperties);
2727
}
2828
if (schema.getAllOf() != null) {
2929
schema.getAllOf().forEach(allOfSchema -> {
3030
var allOfSchemaTyped = (Schema<?>) allOfSchema;
3131
if (allOfSchemaTyped.getProperties() != null) {
3232
var properties = allOfSchemaTyped.getProperties();
33-
processProperties(properties, requiredProperties);
33+
processProperties(schema.getName(), properties, requiredProperties);
3434
}
3535
});
3636
}
3737
schema.setRequired(requiredProperties);
3838
});
3939
}
4040

41-
private static void processProperties(Map<String, Schema> properties, ArrayList<String> requiredProperties) {
41+
@SuppressWarnings("rawtypes")
42+
private static void processProperties(
43+
String modelFqn,
44+
Map<String, Schema> properties,
45+
ArrayList<String> requiredProperties
46+
) {
47+
var cls = loadClass(modelFqn);
48+
if (cls == null) {
49+
return;
50+
}
51+
4252
properties.forEach((propertyName, property) -> {
43-
var isNullable = isNullable(property);
53+
var isNullable = isNullable(cls, propertyName);
4454

4555
if (!isNullable) {
4656
requiredProperties.add(propertyName);
4757
} else {
4858
requiredProperties.remove(propertyName);
4959
}
50-
if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) {
51-
property.setTitle(null);
52-
}
53-
if (property.get$ref() != null) {
54-
property.set$ref(property.get$ref().replace(NULLABLE_MARKER, ""));
55-
}
56-
if (property.getItems() != null && property.getItems().get$ref() != null) {
57-
property.getItems().set$ref(property.getItems().get$ref().replace(NULLABLE_MARKER, ""));
58-
}
5960
});
6061
}
6162

62-
private static boolean isNullable(Schema<?> property) {
63-
if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) {
64-
return true;
63+
@org.jspecify.annotations.Nullable
64+
private static Class<?> loadClass(String fqn) {
65+
try {
66+
return Class.forName(fqn);
67+
} catch (ClassNotFoundException _) {
68+
// if this does not work, we probably have a parameterized type where the fqn is concatenated
6569
}
6670

67-
if (property.get$ref() != null && property.get$ref().endsWith(NULLABLE_MARKER)) {
68-
return true;
71+
var lastDotIndex = -1;
72+
for (var i = 0; i <= fqn.length(); i++) {
73+
if (i == fqn.length() || fqn.charAt(i) == '.') {
74+
var fullPart = fqn.substring(lastDotIndex + 1, i);
75+
if (!fullPart.isEmpty() && Character.isUpperCase(fullPart.charAt(0))) {
76+
// Try the full part first
77+
var baseFqn = fqn.substring(0, i);
78+
try {
79+
return Class.forName(baseFqn);
80+
} catch (ClassNotFoundException _) {
81+
}
82+
83+
// Try stripping capitalized segments from the end of the part
84+
// e.g., LabelAndDescriptionChoiceCom -> try LabelAndDescriptionChoice, then LabelAndDescription, etc.
85+
for (var j = fullPart.length() - 1; j > 0; j--) {
86+
if (Character.isUpperCase(fullPart.charAt(j))) {
87+
var strippedPart = fullPart.substring(0, j);
88+
var candidateFqn = fqn.substring(0, lastDotIndex + 1) + strippedPart;
89+
try {
90+
return Class.forName(candidateFqn);
91+
} catch (ClassNotFoundException _) {
92+
}
93+
}
94+
}
95+
}
96+
lastDotIndex = i;
97+
}
6998
}
99+
return null;
100+
}
101+
102+
private static boolean isNullable(Class<?> cls, String propertyName) {
103+
var currentClass = cls;
104+
while (currentClass != null) {
105+
try {
106+
var field = currentClass.getDeclaredField(propertyName);
107+
if (isNullable(field.getAnnotatedType(), field.getAnnotations())) {
108+
return true;
109+
}
110+
} catch (NoSuchFieldException _) {
111+
}
112+
113+
for (var method : currentClass.getDeclaredMethods()) {
114+
if (method.getName().equals(propertyName)
115+
|| method.getName().equals("get" + capitalize(propertyName))
116+
|| method.getName().equals("is" + capitalize(propertyName))) {
117+
if (isNullable(method.getAnnotatedReturnType(), method.getAnnotations())) {
118+
return true;
119+
}
120+
}
121+
}
70122

71-
if (property.getItems() != null && property.getItems().get$ref() != null && property.getItems()
72-
.get$ref()
73-
.endsWith(NULLABLE_MARKER)) {
74-
return true;
123+
currentClass = currentClass.getSuperclass();
75124
}
76125

77126
return false;
78127
}
128+
129+
private static boolean isNullable(
130+
AnnotatedType annotatedType,
131+
Annotation[] annotations
132+
) {
133+
for (var annotation : annotatedType.getAnnotations()) {
134+
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
135+
return true;
136+
}
137+
}
138+
for (var annotation : annotations) {
139+
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
140+
return true;
141+
}
142+
}
143+
return false;
144+
}
145+
146+
private static String capitalize(String str) {
147+
if (str.isEmpty()) {
148+
return str;
149+
}
150+
return str.substring(0, 1).toUpperCase() + str.substring(1);
151+
}
79152
}

src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java

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

src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformerTest.java

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import org.springframework.beans.factory.annotation.Autowired;
1414

1515
import static org.assertj.core.api.Assertions.assertThat;
16-
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
17+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
1718

1819
@ApplicationTest
1920
@NullMarked
@@ -141,12 +142,10 @@ void givenQueryWithMultipleResults_shouldFail() {
141142

142143
var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");
143144

144-
assertThrows(
145-
IllegalStateException.class,
146-
() -> QueryTransformer
147-
.of(entityManager, TestModelContainer.class)
148-
.withQuery(query)
149-
.asSingleResult()
145+
assertThatIllegalStateException().isThrownBy(() -> QueryTransformer
146+
.of(entityManager, TestModelContainer.class)
147+
.withQuery(query)
148+
.asSingleResult()
150149
);
151150
}
152151
}
@@ -172,11 +171,10 @@ void givenQueryWithOneResult_shouldPass() {
172171
void givenQueryWithOneResult_shouldFail() {
173172
var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");
174173

175-
assertThrows(
176-
EntityNotFoundException.class, () -> QueryTransformer
177-
.of(entityManager, TestModelContainer.class)
178-
.withQuery(query)
179-
.asSingleResultOrFail()
174+
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> QueryTransformer
175+
.of(entityManager, TestModelContainer.class)
176+
.withQuery(query)
177+
.asSingleResultOrFail()
180178
);
181179
}
182180
}
@@ -221,12 +219,10 @@ void givenQuery_wrongTargetClass_shouldFail() {
221219

222220
var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");
223221

224-
assertThrows(
225-
TransformerRuntimeException.class,
226-
() -> QueryTransformer
227-
.of(entityManager, WrongContainer.class)
228-
.withQuery(query)
229-
.asList()
222+
assertThatExceptionOfType(TransformerRuntimeException.class).isThrownBy(() -> QueryTransformer
223+
.of(entityManager, WrongContainer.class)
224+
.withQuery(query)
225+
.asList()
230226
);
231227
}
232228
}
@@ -351,12 +347,10 @@ void givenVariousQueries_shouldPassReturningTheRightTotalCount() {
351347
void givenQueryWithSelectDistinct_shouldFail() {
352348
var query = entityManager.createQuery("select distinct q, 'xxx' from QueryTransformerTestModel q");
353349

354-
assertThrows(
355-
IllegalStateException.class,
356-
() -> QueryTransformer
357-
.of(entityManager, TestModelContainer.class)
358-
.withQuery(query)
359-
.asPage(1, 2)
350+
assertThatIllegalStateException().isThrownBy(() -> QueryTransformer
351+
.of(entityManager, TestModelContainer.class)
352+
.withQuery(query)
353+
.asPage(1, 2)
360354
);
361355
}
362356
}

0 commit comments

Comments
 (0)