11package it .aboutbits .springboot .toolbox .persistence .transformer ;
22
3+ import it .aboutbits .springboot .toolbox .reflection .util .RecordReflectionUtil ;
34import it .aboutbits .springboot .toolbox .type .CustomType ;
4- import it .aboutbits .springboot .toolbox .type .ScaledBigDecimal ;
55
66import java .lang .reflect .Constructor ;
77import java .lang .reflect .Field ;
88import java .lang .reflect .InvocationTargetException ;
99import java .lang .reflect .Modifier ;
10- import java .math .BigDecimal ;
11- import java .math .BigInteger ;
1210import java .time .Instant ;
1311import java .time .OffsetDateTime ;
1412import java .time .ZoneId ;
1513import java .util .Arrays ;
1614
1715@ SuppressWarnings ("rawtypes" )
1816public class TupleTransformer <T > {
19-
2017 private final Class <T > outputClass ;
21- private final Constructor <T > outputClassConstructor ;
22- private final Class [] outputClassFieldClasses ;
18+ private Constructor <T > outputClassConstructor = null ;
19+ private Class [] outputClassFieldClasses = null ;
20+
21+ private final Mode mode ;
2322
24- public TupleTransformer (final Class <T > outputClass ) {
23+ private enum Mode {
24+ PRIMITIVE , // Real Java primitives or their wrapped counterpart (ex., long and Long)
25+ WRAPPED , // Record with a single wrapped value (CustomType as for example Iban)
26+ TUPLE // Complex tuples with more than one value
27+ }
28+
29+ public TupleTransformer (Class <T > outputClass ) {
2530 this .outputClass = outputClass ;
2631
2732 // Find all fields and their types inside the result class
2833 // Ignore constants, because they will not be used as constructor parameters
29- outputClassFieldClasses = Arrays
30- .stream (outputClass .getDeclaredFields ())
31- .filter (field -> !Modifier .isStatic (field .getModifiers ()))
32- .map (Field ::getType )
33- .toArray (Class []::new );
34-
35- // Find the all-args-constructor inside the result class
36- try {
37- outputClassConstructor = outputClass .getDeclaredConstructor (outputClassFieldClasses );
38- outputClassConstructor .setAccessible (true );
39- } catch (NoSuchMethodException exception ) {
40- throw new TransformerRuntimeException (
41- String .format (
42- "Query transformation: Could not find a valid constructor in target class %s" ,
43- outputClass .getName ()
44- ),
45- exception
46- );
34+ if (outputClass .isPrimitive () || isSimpleType (outputClass )) {
35+ mode = Mode .PRIMITIVE ;
36+ } else if (CustomType .class .isAssignableFrom (outputClass )) {
37+ mode = Mode .WRAPPED ;
38+ } else {
39+ mode = Mode .TUPLE ;
40+
41+ outputClassFieldClasses = Arrays
42+ .stream (outputClass .getDeclaredFields ())
43+ .filter (field -> !Modifier .isStatic (field .getModifiers ()))
44+ .map (Field ::getType )
45+ .toArray (Class []::new );
46+
47+ // Find the all-args-constructor inside the result class
48+ try {
49+ outputClassConstructor = outputClass .getDeclaredConstructor (outputClassFieldClasses );
50+ outputClassConstructor .setAccessible (true );
51+ } catch (NoSuchMethodException exception ) {
52+ throw new TransformerRuntimeException (
53+ String .format (
54+ "Query transformation: Could not find a valid constructor in target class %s" ,
55+ outputClass .getName ()
56+ ),
57+ exception
58+ );
59+ }
4760 }
4861 }
4962
63+ private static <T > boolean isSimpleType (Class <T > outputClass ) {
64+ return String .class .isAssignableFrom (outputClass )
65+ || Float .class .isAssignableFrom (outputClass )
66+ || Double .class .isAssignableFrom (outputClass )
67+ || Short .class .isAssignableFrom (outputClass )
68+ || Integer .class .isAssignableFrom (outputClass )
69+ || Long .class .isAssignableFrom (outputClass )
70+ || Boolean .class .isAssignableFrom (outputClass );
71+ }
72+
5073 @ SuppressWarnings ("unchecked" )
51- public T transform (final Object [] objects ) {
74+ public T transform (Object [] objects ) {
5275 try {
76+ if (Mode .PRIMITIVE .equals (mode )) {
77+ if (objects .length != 1 ) {
78+ throw new TransformerRuntimeException ("PRIMITIVE mode does not support multiple values!" );
79+ }
80+ return (T ) objects [0 ];
81+ }
82+
83+ if (Mode .WRAPPED .equals (mode )) {
84+ if (objects .length != 1 ) {
85+ throw new TransformerRuntimeException ("WRAPPED mode does not support multiple values!" );
86+ }
87+ return (T ) toCustomType (objects [0 ], (Class <CustomType <?>>) outputClass );
88+ }
5389
5490 // If we have a single entry in the result, and that entry matches the desired result class
5591 // we can just give it back, no casting, nor type-checking needed. We can just unbox it and
@@ -95,12 +131,6 @@ public T transform(final Object[] objects) {
95131 continue ;
96132 }
97133
98- // Converter: to WrappedValue
99- if (CustomType .class .isAssignableFrom (outputClassFieldClasses [i ])) {
100- objects [i ] = toWrappedValue (objects [i ], outputClassFieldClasses [i ]);
101- continue ;
102- }
103-
104134 // Converter: Instant to OffsetDateTime
105135 if (objects [i ] instanceof Instant && outputClassFieldClasses [i ].isAssignableFrom (OffsetDateTime .class )) {
106136 objects [i ] = OffsetDateTime .ofInstant (
@@ -110,6 +140,12 @@ public T transform(final Object[] objects) {
110140 continue ;
111141 }
112142
143+ // Converter: to Records that wrap exactly one value (CustomType)
144+ if (CustomType .class .isAssignableFrom (outputClassFieldClasses [i ])) {
145+ objects [i ] = toCustomType (objects [i ], outputClassFieldClasses [i ]);
146+ continue ;
147+ }
148+
113149 // Non-matching classes in fields. No converter found...
114150 throw new UnsupportedOperationException (
115151 String .format (
@@ -122,8 +158,13 @@ public T transform(final Object[] objects) {
122158
123159 return outputClassConstructor .newInstance (objects );
124160
125- } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException
126- | UnsupportedOperationException exception ) {
161+ } catch (
162+ InstantiationException
163+ | IllegalAccessException
164+ | NoSuchMethodException
165+ | InvocationTargetException
166+ | UnsupportedOperationException exception
167+ ) {
127168 throw new TransformerRuntimeException (
128169 String .format (
129170 "Query transformation: Given database record cannot be converted into target class %s" ,
@@ -134,55 +175,12 @@ public T transform(final Object[] objects) {
134175 }
135176 }
136177
137- @ SuppressWarnings ("unchecked" )
138- private <X extends CustomType <?>> X toWrappedValue (
178+ private <X extends CustomType <?>> X toCustomType (
139179 Object actualValue ,
140180 Class <X > targetType
141- ) throws InstantiationException , IllegalAccessException , InvocationTargetException , NoSuchMethodException {
142- // types with a preferred constructor
143- if (targetType .isAssignableFrom (ScaledBigDecimal .class )) {
144- var constructor = targetType .getDeclaredConstructor (Double .class );
145- var value = (Double ) actualValue ;
146- return constructor .newInstance (value );
147- }
181+ ) throws InvocationTargetException , InstantiationException , IllegalAccessException {
182+ var constructor = RecordReflectionUtil .getConstructorForType (targetType , actualValue .getClass ());
148183
149- // types using first suitable constructor
150- Constructor <?>[] constructors = targetType .getDeclaredConstructors ();
151-
152- for (Constructor <?> constructor : constructors ) {
153- Class <?>[] parameterTypes = constructor .getParameterTypes ();
154- if (parameterTypes .length == 1 ) {
155- if (Number .class .isAssignableFrom (parameterTypes [0 ])) {
156- if (Long .class .equals (parameterTypes [0 ])) {
157- var val = (Long ) actualValue ;
158- return (X ) constructor .newInstance (val );
159- } else if (Integer .class .equals (parameterTypes [0 ])) {
160- var val = (Integer ) actualValue ;
161- return (X ) constructor .newInstance (val );
162- } else if (Double .class .equals (parameterTypes [0 ])) {
163- var val = (Double ) actualValue ;
164- return (X ) constructor .newInstance (val );
165- } else if (Float .class .equals (parameterTypes [0 ])) {
166- var val = (Float ) actualValue ;
167- return (X ) constructor .newInstance (val );
168- } else if (BigInteger .class .equals (parameterTypes [0 ])) {
169- var val = BigInteger .valueOf ((Long ) actualValue );
170- return (X ) constructor .newInstance (val );
171- } else if (BigDecimal .class .equals (parameterTypes [0 ])) {
172- var val = BigDecimal .valueOf ((Double ) actualValue );
173- return (X ) constructor .newInstance (val );
174- } else {
175- throw new IllegalArgumentException ("Unsupported number type" );
176- }
177- } else if (String .class .equals (parameterTypes [0 ])) {
178- var val = (String ) actualValue ;
179- return (X ) constructor .newInstance (val );
180- } else if (Boolean .class .equals (parameterTypes [0 ])) {
181- var val = (Boolean ) actualValue ;
182- return (X ) constructor .newInstance (val );
183- } // Add more types as needed
184- }
185- }
186- throw new IllegalArgumentException (targetType .getSimpleName () + " does not have a suitable single-value constructor" );
184+ return constructor .newInstance (actualValue );
187185 }
188186}
0 commit comments