Skip to content

Commit a59e753

Browse files
committed
fix query transformer and some other issues
1 parent 3446185 commit a59e753

8 files changed

Lines changed: 568 additions & 391 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package it.aboutbits.springboot.toolbox.persistence.transformer;
2+
3+
import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil;
4+
import it.aboutbits.springboot.toolbox.type.CustomType;
5+
import org.jspecify.annotations.NullMarked;
6+
import org.jspecify.annotations.Nullable;
7+
8+
import java.lang.reflect.InvocationTargetException;
9+
10+
@NullMarked
11+
abstract class BaseTupleTransformer<T> implements org.hibernate.query.TupleTransformer<T> {
12+
protected final Class<T> outputClass;
13+
14+
protected BaseTupleTransformer(Class<T> outputClass) {
15+
this.outputClass = outputClass;
16+
}
17+
18+
@Override
19+
public abstract @Nullable T transformTuple(@Nullable Object[] objects, String[] strings);
20+
21+
protected static <T> boolean isSimpleType(Class<T> outputClass) {
22+
return String.class.isAssignableFrom(outputClass)
23+
|| Float.class.isAssignableFrom(outputClass)
24+
|| Double.class.isAssignableFrom(outputClass)
25+
|| Short.class.isAssignableFrom(outputClass)
26+
|| Integer.class.isAssignableFrom(outputClass)
27+
|| Long.class.isAssignableFrom(outputClass)
28+
|| Character.class.isAssignableFrom(outputClass)
29+
|| Byte.class.isAssignableFrom(outputClass)
30+
|| Boolean.class.isAssignableFrom(outputClass);
31+
}
32+
33+
protected static <X extends CustomType<?>> X toCustomType(
34+
Object actualValue,
35+
Class<X> targetType
36+
) throws InvocationTargetException, InstantiationException, IllegalAccessException {
37+
var constructor = RecordReflectionUtil.getConstructorForType(targetType, actualValue.getClass());
38+
39+
return constructor.newInstance(actualValue);
40+
}
41+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package it.aboutbits.springboot.toolbox.persistence.transformer;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
import org.jspecify.annotations.Nullable;
5+
6+
/**
7+
* Transformer for real Java primitives or their wrapped counterparts (e.g., long and Long).
8+
*/
9+
@NullMarked
10+
class PrimitiveTupleTransformer<T> extends BaseTupleTransformer<T> {
11+
12+
PrimitiveTupleTransformer(Class<T> outputClass) {
13+
super(outputClass);
14+
}
15+
16+
@SuppressWarnings("unchecked")
17+
@Override
18+
public @Nullable T transformTuple(@Nullable Object[] objects, String[] aliases) {
19+
if (objects.length != 1) {
20+
throw new TransformerRuntimeException("PRIMITIVE mode does not support multiple values!");
21+
}
22+
23+
if (objects[0] == null) {
24+
return null;
25+
}
26+
27+
return (T) objects[0];
28+
}
29+
}

src/main/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import jakarta.persistence.TypedQuery;
66
import org.hibernate.query.NativeQuery;
77
import org.hibernate.query.Query;
8-
import org.hibernate.transform.ResultTransformer;
98
import org.jspecify.annotations.NullMarked;
109
import org.jspecify.annotations.Nullable;
1110
import org.springframework.data.domain.Page;
@@ -67,14 +66,13 @@ public T asSingleResultOrFail() {
6766
.orElseThrow(EntityNotFoundException::new);
6867
}
6968

70-
@SuppressWarnings({"deprecation", "unchecked"})
7169
private List<T> asList(@Nullable Integer pageNumber, @Nullable Integer pageSize) {
7270
if (unwrappedQuery == null) {
7371
throw new IllegalStateException("Query not set!");
7472
}
7573

76-
unwrappedQuery.setResultTransformer(
77-
(ResultTransformer<?>) (objects, aliases) -> tupleTransformer.transform(objects)
74+
unwrappedQuery.setTupleTransformer(
75+
tupleTransformer
7876
);
7977

8078
if (pageSize != null && pageNumber != null) {
@@ -83,6 +81,7 @@ private List<T> asList(@Nullable Integer pageNumber, @Nullable Integer pageSize)
8381
.setFirstResult(pageSize * pageNumber);
8482
}
8583

84+
// noinspection unchecked
8685
return (List<T>) unwrappedQuery.getResultList();
8786
}
8887

Lines changed: 20 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,193 +1,39 @@
11
package it.aboutbits.springboot.toolbox.persistence.transformer;
22

3-
import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil;
43
import it.aboutbits.springboot.toolbox.type.CustomType;
54
import org.jspecify.annotations.NullMarked;
65
import org.jspecify.annotations.Nullable;
76

8-
import java.lang.reflect.Constructor;
9-
import java.lang.reflect.Field;
10-
import java.lang.reflect.InvocationTargetException;
11-
import java.lang.reflect.Modifier;
12-
import java.time.Instant;
13-
import java.time.OffsetDateTime;
14-
import java.time.ZoneId;
15-
import java.util.Arrays;
16-
7+
/**
8+
* Factory class that creates the appropriate tuple transformer based on the output class type.
9+
* <p>
10+
* This class maintains backward compatibility while delegating to specific transformer implementations:
11+
* <ul>
12+
* <li>{@link PrimitiveTupleTransformer} - for Java primitives and their wrapped counterparts</li>
13+
* <li>{@link WrappedTupleTransformer} - for records with a single wrapped value (CustomType)</li>
14+
* <li>{@link TupleTupleTransformer} - for complex tuples with more than one value</li>
15+
* </ul>
16+
*/
1717
@NullMarked
18-
public class TupleTransformer<T> {
19-
private final Class<T> outputClass;
20-
private @Nullable Constructor<T> outputClassConstructor = null;
21-
private Class<?> @Nullable [] outputClassFieldClasses = null;
22-
23-
private final Mode mode;
24-
25-
private enum Mode {
26-
PRIMITIVE, // Real Java primitives or their wrapped counterpart (ex., long and Long)
27-
WRAPPED, // Record with a single wrapped value (CustomType as for example Iban)
28-
TUPLE // Complex tuples with more than one value
29-
}
18+
class TupleTransformer<T> extends BaseTupleTransformer<T> {
19+
private final BaseTupleTransformer<T> delegate;
3020

31-
public TupleTransformer(Class<T> outputClass) {
32-
this.outputClass = outputClass;
21+
TupleTransformer(Class<T> outputClass) {
22+
super(outputClass);
3323

3424
// Find all fields and their types inside the result class
3525
// Ignore constants, because they will not be used as constructor parameters
3626
if (outputClass.isPrimitive() || isSimpleType(outputClass)) {
37-
mode = Mode.PRIMITIVE;
27+
delegate = new PrimitiveTupleTransformer<>(outputClass);
3828
} else if (CustomType.class.isAssignableFrom(outputClass)) {
39-
mode = Mode.WRAPPED;
29+
delegate = new WrappedTupleTransformer<>(outputClass);
4030
} else {
41-
mode = Mode.TUPLE;
42-
43-
outputClassFieldClasses = Arrays
44-
.stream(outputClass.getDeclaredFields())
45-
.filter(field -> !Modifier.isStatic(field.getModifiers()))
46-
.map(Field::getType)
47-
.toArray(Class[]::new);
48-
49-
// Find the all-args-constructor inside the result class
50-
try {
51-
outputClassConstructor = outputClass.getDeclaredConstructor(outputClassFieldClasses);
52-
outputClassConstructor.setAccessible(true);
53-
} catch (NoSuchMethodException exception) {
54-
throw new TransformerRuntimeException(
55-
String.format(
56-
"Query transformation: Could not find a valid constructor in target class %s",
57-
outputClass.getName()
58-
),
59-
exception
60-
);
61-
}
62-
}
63-
}
64-
65-
@SuppressWarnings("unchecked")
66-
public T transform(Object[] objects) {
67-
try {
68-
if (Mode.PRIMITIVE.equals(mode)) {
69-
if (objects.length != 1) {
70-
throw new TransformerRuntimeException("PRIMITIVE mode does not support multiple values!");
71-
}
72-
return (T) objects[0];
73-
}
74-
75-
if (Mode.WRAPPED.equals(mode)) {
76-
if (objects.length != 1) {
77-
throw new TransformerRuntimeException("WRAPPED mode does not support multiple values!");
78-
}
79-
return (T) toCustomType(objects[0], (Class<CustomType<?>>) outputClass);
80-
}
81-
82-
// If we have a single entry in the result, and that entry matches the desired result class
83-
// we can just give it back, no casting, nor type-checking needed. We can just unbox it and
84-
// give it back as-is!
85-
// Example: "SELECT p FROM Person p"
86-
if (objects.length == 1 && outputClass == objects[0].getClass()) {
87-
return (T) objects[0];
88-
}
89-
90-
if (outputClassFieldClasses == null || objects.length != outputClassFieldClasses.length) {
91-
throw new TransformerRuntimeException(
92-
String.format(
93-
"Invalid query transforming: object count does not match target class field count for %s",
94-
outputClass.getName()
95-
)
96-
);
97-
}
98-
99-
// Unboxing not possible, we have a complex combined result, check single record entries for type-safety!
100-
for (var i = 0; i < objects.length; i++) {
101-
102-
// Everything ok, null matches every object and equal classes do not need casting!
103-
// Unboxing of primitives is automatic when we call the constructor of the target result class.
104-
if (objects[i] == null || outputClassFieldClasses[i] == null || outputClassFieldClasses[i].isPrimitive() || objects[i].getClass() == outputClassFieldClasses[i]) {
105-
continue;
106-
}
107-
108-
// Check if the two classes are either the same, or if it is a superclass or superinterface of it...
109-
// For example, casting an ArrayList to List can be done directly
110-
if (outputClassFieldClasses[i].isAssignableFrom(objects[i].getClass())) {
111-
objects[i] = outputClassFieldClasses[i].cast(objects[i]);
112-
continue;
113-
}
114-
115-
// Converter: STRING to ENUM
116-
// A string from the DB, that does not match a corresponding field inside the result class
117-
// should probably be an enum value, which implements the "valueOf" interface.
118-
if (objects[i] instanceof String && outputClassFieldClasses[i].isEnum()) {
119-
objects[i] = outputClassFieldClasses[i].getMethod("valueOf", String.class).invoke(
120-
null,
121-
objects[i].toString()
122-
);
123-
continue;
124-
}
125-
126-
// Converter: Instant to OffsetDateTime
127-
if (objects[i] instanceof Instant instant && outputClassFieldClasses[i].isAssignableFrom(OffsetDateTime.class)) {
128-
objects[i] = OffsetDateTime.ofInstant(
129-
instant,
130-
ZoneId.systemDefault()
131-
);
132-
continue;
133-
}
134-
135-
// Converter: to Records that wrap exactly one value (CustomType)
136-
if (CustomType.class.isAssignableFrom(outputClassFieldClasses[i])) {
137-
objects[i] = toCustomType(objects[i], (Class<? extends CustomType<?>>) outputClassFieldClasses[i]);
138-
continue;
139-
}
140-
141-
// Non-matching classes in fields. No converter found...
142-
throw new UnsupportedOperationException(
143-
String.format(
144-
"Query transformation: Type mismatch without converter. Cannot cast from %s to %s.",
145-
objects[i].getClass().getName(),
146-
outputClassFieldClasses[i].getName()
147-
)
148-
);
149-
}
150-
151-
if (outputClassConstructor == null) {
152-
throw new IllegalStateException("Constructor not initialized!");
153-
}
154-
155-
return outputClassConstructor.newInstance(objects);
156-
} catch (
157-
InstantiationException
158-
| IllegalAccessException
159-
| NoSuchMethodException
160-
| InvocationTargetException
161-
| UnsupportedOperationException exception
162-
) {
163-
throw new TransformerRuntimeException(
164-
String.format(
165-
"Query transformation: Given database record cannot be converted into target class %s",
166-
outputClass.getName()
167-
),
168-
exception
169-
);
31+
delegate = new TupleTupleTransformer<>(outputClass);
17032
}
17133
}
17234

173-
private static <T> boolean isSimpleType(Class<T> outputClass) {
174-
return String.class.isAssignableFrom(outputClass)
175-
|| Float.class.isAssignableFrom(outputClass)
176-
|| Double.class.isAssignableFrom(outputClass)
177-
|| Short.class.isAssignableFrom(outputClass)
178-
|| Integer.class.isAssignableFrom(outputClass)
179-
|| Long.class.isAssignableFrom(outputClass)
180-
|| Character.class.isAssignableFrom(outputClass)
181-
|| Byte.class.isAssignableFrom(outputClass)
182-
|| Boolean.class.isAssignableFrom(outputClass);
183-
}
184-
185-
private static <X extends CustomType<?>> X toCustomType(
186-
Object actualValue,
187-
Class<X> targetType
188-
) throws InvocationTargetException, InstantiationException, IllegalAccessException {
189-
var constructor = RecordReflectionUtil.getConstructorForType(targetType, actualValue.getClass());
190-
191-
return constructor.newInstance(actualValue);
35+
@Override
36+
public @Nullable T transformTuple(@Nullable Object[] objects, String[] aliases) {
37+
return delegate.transformTuple(objects, aliases);
19238
}
19339
}

0 commit comments

Comments
 (0)