Skip to content

Commit de74f0e

Browse files
committed
add wrapped values and primitive support to tuple transformer
1 parent db1e4d0 commit de74f0e

5 files changed

Lines changed: 232 additions & 106 deletions

File tree

src/main/java/it/aboutbits/springboot/toolbox/.gitkeep

Whitespace-only changes.

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import jakarta.persistence.EntityNotFoundException;
55
import jakarta.persistence.Query;
66
import jakarta.persistence.TypedQuery;
7+
import lombok.SneakyThrows;
78
import org.hibernate.query.NativeQuery;
89
import org.hibernate.transform.ResultTransformer;
910
import org.springframework.data.domain.Page;
@@ -13,36 +14,39 @@
1314
import java.util.List;
1415
import java.util.Optional;
1516

16-
@SuppressWarnings({"rawtypes"})
17+
@SuppressWarnings("rawtypes")
1718
public final class QueryTransformer<T> {
1819

1920
private final EntityManager entityManager;
2021
private final TupleTransformer<T> tupleTransformer;
2122
private org.hibernate.query.Query unwrappedQuery;
23+
private Query query;
2224
private boolean isNative = false;
2325

24-
private QueryTransformer(final EntityManager entityManager, final Class<T> outputClass) {
26+
private QueryTransformer(EntityManager entityManager, Class<T> outputClass) {
2527
this.entityManager = entityManager;
2628
this.tupleTransformer = new TupleTransformer<>(outputClass);
29+
2730
}
2831

29-
public static <T> QueryTransformer<T> of(final EntityManager entityManager, final Class<T> outputClass) {
32+
public static <T> QueryTransformer<T> of(EntityManager entityManager, Class<T> outputClass) {
3033
return new QueryTransformer<>(entityManager, outputClass);
3134
}
3235

33-
public QueryTransformer<T> withQuery(final Query query) {
36+
public QueryTransformer<T> withQuery(Query query) {
3437
if (query instanceof NativeQuery<?>) {
3538
this.isNative = true;
3639
}
3740
this.unwrappedQuery = query.unwrap(org.hibernate.query.Query.class);
41+
this.query = query;
3842
return this;
3943
}
4044

4145
public Page<T> asPage(Pageable pageable) {
4246
return asPage(pageable.getPageNumber(), pageable.getPageSize());
4347
}
4448

45-
public Page<T> asPage(final int pageNumber, final int pageSize) {
49+
public Page<T> asPage(int pageNumber, int pageSize) {
4650
return isNative ? asPageNativeQuery(pageNumber, pageSize) : asPageQuery(pageNumber, pageSize);
4751
}
4852

@@ -58,7 +62,7 @@ public Optional<T> asSingleResult() {
5862
if (result.size() > 1) {
5963
throw new IllegalStateException("Single result query returned multiple results!");
6064
}
61-
return Optional.of(result.get(0));
65+
return Optional.of(result.getFirst());
6266
}
6367

6468
public T asSingleResultOrFail() {
@@ -67,7 +71,7 @@ public T asSingleResultOrFail() {
6771
}
6872

6973
@SuppressWarnings({"deprecation", "unchecked"})
70-
private List<T> asList(final Integer pageNumber, final Integer pageSize) {
74+
private List<T> asList(Integer pageNumber, Integer pageSize) {
7175
unwrappedQuery.setResultTransformer(
7276
(ResultTransformer) (objects, aliases) -> tupleTransformer.transform(objects)
7377
);
@@ -81,14 +85,16 @@ private List<T> asList(final Integer pageNumber, final Integer pageSize) {
8185
return unwrappedQuery.getResultList();
8286
}
8387

84-
private Page<T> asPageQuery(final int pageNumber, final int pageSize) {
88+
@SneakyThrows
89+
private Page<T> asPageQuery(int pageNumber, int pageSize) {
8590
var selectPattern = "(?i)select.*?[ \\t]*from ";
8691
var queryString = unwrappedQuery.getQueryString().trim().replaceAll("\\R", " ");
8792
var countQueryString = queryString.replaceFirst(selectPattern, "select count(*) from ");
8893
countQueryString = countQueryString.replaceAll("(?i)\\s+order\\s+by\\s+.*$", "");
8994

9095
if (queryString.toLowerCase().contains("select distinct")) {
91-
throw new IllegalStateException("Pagination is not possible, if SELECT DISTINCT is present");
96+
throw new IllegalStateException(
97+
"Pagination is not possible, if SELECT DISTINCT is present. Remove DISTINCT and use GROUP BY instead!");
9298
}
9399

94100
if (countQueryString.equals(queryString)) {
@@ -109,11 +115,13 @@ private Page<T> asPageQuery(final int pageNumber, final int pageSize) {
109115
return new PageImpl<>(content, Pageable.ofSize(pageSize).withPage(pageNumber), count);
110116
}
111117

112-
private Page<T> asPageNativeQuery(final int pageNumber, final int pageSize) {
118+
private Page<T> asPageNativeQuery(int pageNumber, int pageSize) {
113119
var queryString = unwrappedQuery.getQueryString().trim().replaceAll("\\R", " ");
114120
var countQueryString = "select count(*) from (" + queryString + ") as count";
115121
var parameters = unwrappedQuery.getParameters();
116-
var countQuery = isNative ? entityManager.createNativeQuery(countQueryString, Long.class) : entityManager.createQuery(countQueryString, Long.class);
122+
var countQuery = isNative
123+
? entityManager.createNativeQuery(countQueryString, Long.class)
124+
: entityManager.createQuery(countQueryString, Long.class);
117125
for (var parameter : parameters) {
118126
var value = unwrappedQuery.getParameterValue(parameter.getPosition());
119127
countQuery.setParameter(parameter.getPosition(), value);
@@ -129,7 +137,7 @@ private Page<T> asPageNativeQuery(final int pageNumber, final int pageSize) {
129137
* So, if we find a "group by" inside the query string we just count the groups and do not sum the count within
130138
* them.
131139
*/
132-
private static long getCount(final TypedQuery<Long> countQuery, final String queryString) {
140+
private static long getCount(TypedQuery<Long> countQuery, String queryString) {
133141
var countQueryResults = countQuery.getResultList();
134142
if (countQueryResults == null || countQueryResults.isEmpty()) {
135143
return 0L;
@@ -141,15 +149,15 @@ private static long getCount(final TypedQuery<Long> countQuery, final String que
141149
}
142150

143151
// Non-grouping query: return the first element, which is the result of count(*)
144-
return countQueryResults.get(0);
152+
return countQueryResults.getFirst();
145153
}
146154

147-
private static long getCount(final Query countQuery) {
155+
private static long getCount(Query countQuery) {
148156
var countQueryResults = countQuery.getResultList();
149157
if (countQueryResults == null || countQueryResults.isEmpty()) {
150158
return 0L;
151159
}
152160
// Non-grouping query: return the first element, which is the result of count(*)
153-
return (long) countQueryResults.get(0);
161+
return (long) countQueryResults.getFirst();
154162
}
155163
}
Lines changed: 79 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,91 @@
11
package it.aboutbits.springboot.toolbox.persistence.transformer;
22

3+
import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil;
34
import it.aboutbits.springboot.toolbox.type.CustomType;
4-
import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
55

66
import java.lang.reflect.Constructor;
77
import java.lang.reflect.Field;
88
import java.lang.reflect.InvocationTargetException;
99
import java.lang.reflect.Modifier;
10-
import java.math.BigDecimal;
11-
import java.math.BigInteger;
1210
import java.time.Instant;
1311
import java.time.OffsetDateTime;
1412
import java.time.ZoneId;
1513
import java.util.Arrays;
1614

1715
@SuppressWarnings("rawtypes")
1816
public 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

Comments
 (0)