Skip to content

Commit 54414d5

Browse files
authored
add copy constructors to custom type for jpa implicit dto projection (#53)
* add copy constructors to custom type for jpa implicit dto projection * ignore AI file * fix swagger nullability for collections * fix class scanning of sub-types
1 parent f8cb6ed commit 54414d5

11 files changed

Lines changed: 328 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ build/
4646

4747
### Local Development ###
4848
application-local.yml
49+
/.output.txt

src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ public String[] getScannedPackages() {
3636

3737
@SuppressWarnings("unchecked")
3838
public <T> Set<Class<? extends T>> getSubTypesOf(Class<T> clazz) {
39-
return scanResult.getClassesImplementing(clazz).loadClasses()
39+
var classInfoList = clazz.isInterface()
40+
? scanResult.getClassesImplementing(clazz)
41+
: scanResult.getSubclasses(clazz);
42+
return classInfoList.loadClasses()
4043
.stream()
4144
.map(item -> (Class<? extends T>) item)
4245
.collect(Collectors.toSet());

src/main/java/it/aboutbits/springboot/toolbox/swagger/SwaggerMeta.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ public class SwaggerMeta {
3434

3535
@Nullable
3636
private String mapKeyTypeFqn = null;
37+
38+
@Nullable
39+
@JsonProperty("isNullable")
40+
private Boolean isNullable = null;
3741
}

src/main/java/it/aboutbits/springboot/toolbox/swagger/SwaggerMetaUtil.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ public static String setIsNestedStructure(@Nullable String currentMeta, boolean
6363
return OBJECT_MAPPER.writeValueAsString(meta);
6464
}
6565

66+
@SneakyThrows(JsonProcessingException.class)
67+
public static String setIsNullable(@Nullable String currentMeta, boolean value) {
68+
var meta = getSwaggerMeta(currentMeta);
69+
meta.setIsNullable(!value ? null : true);
70+
71+
return OBJECT_MAPPER.writeValueAsString(meta);
72+
}
73+
6674
private static SwaggerMeta getSwaggerMeta(@Nullable String currentMeta) {
6775
var meta = new SwaggerMeta();
6876
if (currentMeta != null) {

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
import io.swagger.v3.oas.models.OpenAPI;
44
import io.swagger.v3.oas.models.media.Schema;
5+
import it.aboutbits.springboot.toolbox.swagger.SwaggerMetaUtil;
56
import org.jspecify.annotations.NullMarked;
67
import org.springdoc.core.customizers.OpenApiCustomizer;
78

89
import java.lang.annotation.Annotation;
10+
import java.lang.reflect.AnnotatedArrayType;
11+
import java.lang.reflect.AnnotatedParameterizedType;
912
import java.lang.reflect.AnnotatedType;
13+
import java.lang.reflect.ParameterizedType;
1014
import java.util.ArrayList;
15+
import java.util.Collection;
1116
import java.util.Map;
1217

1318
@NullMarked
@@ -57,9 +62,98 @@ private static void processProperties(
5762
} else {
5863
requiredProperties.remove(propertyName);
5964
}
65+
66+
// Check for nullable type parameters in collections/arrays
67+
var annotatedType = getAnnotatedType(cls, propertyName);
68+
if (annotatedType != null) {
69+
var nullableDepths = new ArrayList<Integer>();
70+
findNullableDepths(annotatedType, 0, nullableDepths);
71+
// Add "nullable" description at each depth where nullable elements are found
72+
for (var depth : nullableDepths) {
73+
addNullableDescriptionAtDepth(property, depth);
74+
}
75+
}
6076
});
6177
}
6278

79+
@org.jspecify.annotations.Nullable
80+
private static AnnotatedType getAnnotatedType(Class<?> cls, String propertyName) {
81+
var currentClass = cls;
82+
while (currentClass != null) {
83+
try {
84+
var field = currentClass.getDeclaredField(propertyName);
85+
return field.getAnnotatedType();
86+
} catch (NoSuchFieldException _) {
87+
}
88+
89+
for (var method : currentClass.getDeclaredMethods()) {
90+
if (method.getName().equals(propertyName)
91+
|| method.getName().equals("get" + capitalize(propertyName))
92+
|| method.getName().equals("is" + capitalize(propertyName))) {
93+
return method.getAnnotatedReturnType();
94+
}
95+
}
96+
97+
currentClass = currentClass.getSuperclass();
98+
}
99+
return null;
100+
}
101+
102+
private static void findNullableDepths(AnnotatedType annotatedType, int depth, ArrayList<Integer> nullableDepths) {
103+
if (annotatedType instanceof AnnotatedParameterizedType parameterizedType) {
104+
var rawType = parameterizedType.getType();
105+
if (rawType instanceof ParameterizedType pt) {
106+
var rawClass = pt.getRawType();
107+
if (rawClass instanceof Class<?> clazz && isCollectionType(clazz)) {
108+
var typeArgs = parameterizedType.getAnnotatedActualTypeArguments();
109+
for (var typeArg : typeArgs) {
110+
if (hasNullableAnnotation(typeArg)) {
111+
nullableDepths.add(depth);
112+
}
113+
// Recursively check nested type parameters
114+
findNullableDepths(typeArg, depth + 1, nullableDepths);
115+
}
116+
}
117+
}
118+
} else if (annotatedType instanceof AnnotatedArrayType arrayType) {
119+
var componentType = arrayType.getAnnotatedGenericComponentType();
120+
if (hasNullableAnnotation(componentType)) {
121+
nullableDepths.add(depth);
122+
}
123+
// Recursively check nested array types
124+
findNullableDepths(componentType, depth + 1, nullableDepths);
125+
}
126+
}
127+
128+
@SuppressWarnings("rawtypes")
129+
private static void addNullableDescriptionAtDepth(Schema<?> schema, int depth) {
130+
Schema currentSchema = schema;
131+
for (int i = 0; i <= depth; i++) {
132+
var items = currentSchema.getItems();
133+
if (items == null) {
134+
return; // Schema structure doesn't match expected depth
135+
}
136+
currentSchema = items;
137+
}
138+
currentSchema.setDescription(SwaggerMetaUtil.setIsNullable(
139+
currentSchema.getDescription(),
140+
true
141+
));
142+
}
143+
144+
private static boolean isCollectionType(Class<?> clazz) {
145+
return Collection.class.isAssignableFrom(clazz) || clazz.isArray();
146+
}
147+
148+
private static boolean hasNullableAnnotation(AnnotatedType annotatedType) {
149+
for (var annotation : annotatedType.getAnnotations()) {
150+
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
151+
return true;
152+
}
153+
}
154+
return false;
155+
}
156+
63157
@org.jspecify.annotations.Nullable
64158
private static Class<?> loadClass(String fqn) {
65159
try {

src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public EmailAddress(@Nullable String value) {
2626
this.value = value.toLowerCase();
2727
}
2828

29+
@SuppressWarnings("unused")
30+
EmailAddress(EmailAddress other) {
31+
this(other.value);
32+
}
33+
2934
@Override
3035
public String toString() {
3136
return value;

src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public Iban(@Nullable String value) {
2222
this.value = value.toUpperCase();
2323
}
2424

25+
@SuppressWarnings("unused")
26+
Iban(Iban other) {
27+
this(other.value);
28+
}
29+
2530
@Override
2631
public String toString() {
2732
return value;

src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public record ScaledBigDecimal(
2323
public static final ScaledBigDecimal TWO = new ScaledBigDecimal(2);
2424
public static final ScaledBigDecimal TEN = new ScaledBigDecimal(10);
2525

26+
@SuppressWarnings("unused")
27+
ScaledBigDecimal(ScaledBigDecimal other) {
28+
this(other.value);
29+
}
30+
2631
public ScaledBigDecimal(BigDecimal value) {
2732
this.value = value.setScale(MATH_CONTEXT.getPrecision(), MATH_CONTEXT.getRoundingMode());
2833
}

0 commit comments

Comments
 (0)