|
1 | 1 | package it.aboutbits.springboot.toolbox.persistence.transformer; |
2 | 2 |
|
3 | | -import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; |
4 | 3 | import it.aboutbits.springboot.toolbox.type.CustomType; |
5 | 4 | import org.jspecify.annotations.NullMarked; |
6 | 5 | import org.jspecify.annotations.Nullable; |
7 | 6 |
|
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 | + */ |
17 | 17 | @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; |
30 | 20 |
|
31 | | - public TupleTransformer(Class<T> outputClass) { |
32 | | - this.outputClass = outputClass; |
| 21 | + TupleTransformer(Class<T> outputClass) { |
| 22 | + super(outputClass); |
33 | 23 |
|
34 | 24 | // Find all fields and their types inside the result class |
35 | 25 | // Ignore constants, because they will not be used as constructor parameters |
36 | 26 | if (outputClass.isPrimitive() || isSimpleType(outputClass)) { |
37 | | - mode = Mode.PRIMITIVE; |
| 27 | + delegate = new PrimitiveTupleTransformer<>(outputClass); |
38 | 28 | } else if (CustomType.class.isAssignableFrom(outputClass)) { |
39 | | - mode = Mode.WRAPPED; |
| 29 | + delegate = new WrappedTupleTransformer<>(outputClass); |
40 | 30 | } 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); |
170 | 32 | } |
171 | 33 | } |
172 | 34 |
|
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); |
192 | 38 | } |
193 | 39 | } |
0 commit comments