Skip to content

Commit 4ac34c2

Browse files
mp911dechristophstrobl
authored andcommitted
Polishing.
Introduce constructor compatibility check by checking assignable types. Original Pull Request: #3654
1 parent 7eb313e commit 4ac34c2

File tree

2 files changed

+188
-53
lines changed

2 files changed

+188
-53
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

+77-46
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import jakarta.persistence.TypedQuery;
2525

2626
import java.lang.reflect.Constructor;
27+
import java.util.ArrayList;
2728
import java.util.Arrays;
2829
import java.util.Collection;
2930
import java.util.HashMap;
@@ -34,6 +35,7 @@
3435
import java.util.stream.Collectors;
3536

3637
import org.springframework.beans.BeanUtils;
38+
import org.springframework.core.MethodParameter;
3739
import org.springframework.core.convert.converter.Converter;
3840
import org.springframework.data.jpa.provider.PersistenceProvider;
3941
import org.springframework.data.jpa.repository.EntityGraph;
@@ -56,7 +58,6 @@
5658
import org.springframework.lang.Nullable;
5759
import org.springframework.util.Assert;
5860
import org.springframework.util.ClassUtils;
59-
import org.springframework.util.ReflectionUtils;
6061

6162
/**
6263
* Abstract base class to implement {@link RepositoryQuery}s.
@@ -347,7 +348,7 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) {
347348
&& !type.getInputProperties().isEmpty();
348349

349350
if (this.dtoProjection) {
350-
this.preferredConstructor = PreferredConstructorDiscoverer.discover(type.getReturnedType());
351+
this.preferredConstructor = PreferredConstructorDiscoverer.discover(type.getReturnedType());
351352
} else {
352353
this.preferredConstructor = null;
353354
}
@@ -373,67 +374,97 @@ public Object convert(Object source) {
373374

374375
if (dtoProjection) {
375376

376-
Object[] ctorArgs = new Object[type.getInputProperties().size()];
377+
Object[] ctorArgs = new Object[elements.size()];
378+
for (int i = 0; i < ctorArgs.length; i++) {
379+
ctorArgs[i] = tuple.get(i);
380+
}
381+
382+
List<Class<?>> argTypes = getArgumentTypes(ctorArgs);
377383

378-
boolean containsNullValue = false;
379-
for (int i = 0; i < type.getInputProperties().size(); i++) {
380-
Object value = tuple.get(i);
381-
ctorArgs[i] = value;
382-
if (!containsNullValue && value == null) {
383-
containsNullValue = true;
384-
}
384+
if (preferredConstructor != null && isConstructorCompatible(preferredConstructor.getConstructor(), argTypes)) {
385+
return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs);
385386
}
386387

387-
try {
388+
return BeanUtils.instantiateClass(getFirstMatchingConstructor(ctorArgs, argTypes), ctorArgs);
389+
}
388390

389-
if (preferredConstructor != null && preferredConstructor.getParameterCount() == ctorArgs.length) {
390-
return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs);
391-
}
391+
return new TupleBackedMap(tupleWrapper.apply(tuple));
392+
}
392393

393-
Constructor<?> ctor = null;
394+
private Constructor<?> getFirstMatchingConstructor(Object[] ctorArgs, List<Class<?>> argTypes) {
394395

395-
if (!containsNullValue) { // let's seem if we have an argument type match
396-
ctor = type.getReturnedType()
397-
.getConstructor(Arrays.stream(ctorArgs).map(Object::getClass).toArray(Class<?>[]::new));
398-
}
396+
for (Constructor<?> ctor : type.getReturnedType().getDeclaredConstructors()) {
399397

400-
if (ctor == null) { // let's see if there's more general constructor we could use that accepts our args
401-
ctor = findFirstMatchingConstructor(ctorArgs);
402-
}
398+
if (ctor.getParameterCount() != ctorArgs.length) {
399+
continue;
400+
}
403401

404-
if (ctor != null) {
405-
return BeanUtils.instantiateClass(ctor, ctorArgs);
406-
}
407-
} catch (ReflectiveOperationException e) {
408-
ReflectionUtils.handleReflectionException(e);
402+
if (isConstructorCompatible(ctor, argTypes)) {
403+
return ctor;
409404
}
410405
}
411406

412-
return new TupleBackedMap(tupleWrapper.apply(tuple));
407+
throw new IllegalStateException(String.format(
408+
"Cannot find compatible constructor for DTO projection '%s' accepting '%s'", type.getReturnedType().getName(),
409+
argTypes.stream().map(Class::getName).collect(Collectors.joining(", "))));
413410
}
414411

415-
@Nullable
416-
private Constructor<?> findFirstMatchingConstructor(Object[] ctorArgs) {
412+
private static List<Class<?>> getArgumentTypes(Object[] ctorArgs) {
413+
List<Class<?>> argTypes = new ArrayList<>(ctorArgs.length);
417414

418-
for (Constructor<?> ctor : type.getReturnedType().getDeclaredConstructors()) {
415+
for (Object ctorArg : ctorArgs) {
416+
argTypes.add(ctorArg == null ? Void.class : ctorArg.getClass());
417+
}
418+
return argTypes;
419+
}
419420

420-
if (ctor.getParameterCount() == ctorArgs.length) {
421-
boolean itsAMatch = true;
422-
for (int i = 0; i < ctor.getParameterCount(); i++) {
423-
if (ctorArgs[i] == null) {
424-
continue;
425-
}
426-
if (!ClassUtils.isAssignable(ctor.getParameterTypes()[i], ctorArgs[i].getClass())) {
427-
itsAMatch = false;
428-
break;
429-
}
430-
}
431-
if (itsAMatch) {
432-
return ctor;
433-
}
421+
public static boolean isConstructorCompatible(Constructor<?> constructor, List<Class<?>> argumentTypes) {
422+
423+
if (constructor.getParameterCount() != argumentTypes.size()) {
424+
return false;
425+
}
426+
427+
for (int i = 0; i < argumentTypes.size(); i++) {
428+
429+
MethodParameter methodParameter = MethodParameter.forExecutable(constructor, i);
430+
Class<?> argumentType = argumentTypes.get(i);
431+
432+
if (!areAssignmentCompatible(methodParameter.getParameterType(), argumentType)) {
433+
return false;
434434
}
435435
}
436-
return null;
436+
return true;
437+
}
438+
439+
private static boolean areAssignmentCompatible(Class<?> to, Class<?> from) {
440+
441+
if (from == Void.class && !to.isPrimitive()) {
442+
// treat Void as the bottom type, the class of null
443+
return true;
444+
}
445+
446+
if (to.isPrimitive()) {
447+
448+
if (to == Short.TYPE) {
449+
return from == Character.class || from == Byte.class;
450+
}
451+
452+
if (to == Integer.TYPE) {
453+
return from == Short.class || from == Character.class || from == Byte.class;
454+
}
455+
456+
if (to == Long.TYPE) {
457+
return from == Integer.class || from == Short.class || from == Character.class || from == Byte.class;
458+
}
459+
460+
if (to == Double.TYPE) {
461+
return from == Float.class;
462+
}
463+
464+
return ClassUtils.isAssignable(to, from);
465+
}
466+
467+
return ClassUtils.isAssignable(to, from);
437468
}
438469

439470
/**

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java

+111-7
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import jakarta.persistence.Tuple;
22+
import jakarta.persistence.TupleElement;
23+
2124
import java.util.Arrays;
2225
import java.util.Collections;
2326
import java.util.List;
2427
import java.util.Map;
2528

26-
import jakarta.persistence.Tuple;
27-
import jakarta.persistence.TupleElement;
28-
2929
import org.assertj.core.api.SoftAssertions;
3030
import org.junit.jupiter.api.BeforeEach;
3131
import org.junit.jupiter.api.Test;
@@ -49,6 +49,8 @@
4949
*
5050
* @author Oliver Gierke
5151
* @author Jens Schauder
52+
* @author Christoph Strobl
53+
* @author Mark Paluch
5254
* @soundtrack James Bay - Let it go (Chaos and the Calm)
5355
*/
5456
@ExtendWith(MockitoExtension.class)
@@ -115,13 +117,88 @@ void findsValuesForAllVariantsSupportedByTheTuple() {
115117
}
116118

117119
@Test // GH-3076
118-
void dealsWithNullsInArgumetents() {
120+
void dealsWithNullsInArguments() {
119121

120122
ReturnedType returnedType = ReturnedType.of(WithPC.class, DomainType.class, new SpelAwareProxyProjectionFactory());
121123

124+
doReturn(List.of(element, element, element)).when(tuple).getElements();
125+
when(tuple.get(eq(0))).thenReturn("one");
126+
when(tuple.get(eq(1))).thenReturn(null);
127+
when(tuple.get(eq(2))).thenReturn(1);
128+
129+
Object result = new TupleConverter(returnedType).convert(tuple);
130+
assertThat(result).isInstanceOf(WithPC.class);
131+
}
132+
133+
@Test // GH-3076
134+
void fallsBackToCompatibleConstructor() {
135+
136+
ReturnedType returnedType = spy(
137+
ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory()));
138+
when(returnedType.isProjecting()).thenReturn(true);
139+
when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two", "three"));
140+
141+
doReturn(List.of(element, element, element)).when(tuple).getElements();
142+
when(tuple.get(eq(0))).thenReturn("one");
143+
when(tuple.get(eq(1))).thenReturn(null);
144+
when(tuple.get(eq(2))).thenReturn(2);
145+
146+
MultipleConstructors result = (MultipleConstructors) new TupleConverter(returnedType).convert(tuple);
147+
148+
assertThat(result.one).isEqualTo("one");
149+
assertThat(result.two).isNull();
150+
assertThat(result.three).isEqualTo(2);
151+
152+
reset(tuple);
153+
154+
doReturn(List.of(element, element, element)).when(tuple).getElements();
155+
when(tuple.get(eq(0))).thenReturn("one");
156+
when(tuple.get(eq(1))).thenReturn(null);
157+
when(tuple.get(eq(2))).thenReturn('a');
158+
159+
result = (MultipleConstructors) new TupleConverter(returnedType).convert(tuple);
160+
161+
assertThat(result.one).isEqualTo("one");
162+
assertThat(result.two).isNull();
163+
assertThat(result.three).isEqualTo(97);
164+
}
165+
166+
@Test // GH-3076
167+
void acceptsConstructorWithCastableType() {
168+
169+
ReturnedType returnedType = spy(
170+
ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory()));
171+
when(returnedType.isProjecting()).thenReturn(true);
172+
when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two", "three", "four"));
173+
174+
doReturn(List.of(element, element, element, element)).when(tuple).getElements();
122175
when(tuple.get(eq(0))).thenReturn("one");
123176
when(tuple.get(eq(1))).thenReturn(null);
124-
new TupleConverter(returnedType).convert(tuple);
177+
when(tuple.get(eq(2))).thenReturn((byte) 2);
178+
when(tuple.get(eq(3))).thenReturn(2.1f);
179+
180+
MultipleConstructors result = (MultipleConstructors) new TupleConverter(returnedType).convert(tuple);
181+
182+
assertThat(result.one).isEqualTo("one");
183+
assertThat(result.two).isNull();
184+
assertThat(result.three).isEqualTo(2);
185+
assertThat(result.four).isEqualTo(2, offset(0.1d));
186+
}
187+
188+
@Test // GH-3076
189+
void failsForNonResolvableConstructor() {
190+
191+
ReturnedType returnedType = spy(
192+
ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory()));
193+
when(returnedType.isProjecting()).thenReturn(true);
194+
when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two"));
195+
196+
doReturn(List.of(element, element)).when(tuple).getElements();
197+
when(tuple.get(eq(0))).thenReturn(1);
198+
when(tuple.get(eq(1))).thenReturn(null);
199+
200+
assertThatIllegalStateException().isThrownBy(() -> new TupleConverter(returnedType).convert(tuple))
201+
.withMessageContaining("Cannot find compatible constructor for DTO projection");
125202
}
126203

127204
interface SampleRepository extends CrudRepository<Object, Long> {
@@ -193,13 +270,40 @@ static class DomainType {
193270
String one, two, three;
194271
}
195272

196-
static class WithPC {
273+
static class WithPC {
197274
String one;
198275
String two;
276+
long three;
199277

200-
public WithPC(String one, String two) {
278+
public WithPC(String one, String two, long three) {
201279
this.one = one;
202280
this.two = two;
281+
this.three = three;
203282
}
204283
}
284+
285+
static class MultipleConstructors {
286+
String one;
287+
String two;
288+
long three;
289+
double four;
290+
291+
public MultipleConstructors(String one) {
292+
this.one = one;
293+
}
294+
295+
public MultipleConstructors(String one, String two, long three) {
296+
this.one = one;
297+
this.two = two;
298+
this.three = three;
299+
}
300+
301+
public MultipleConstructors(String one, String two, short three, double four) {
302+
this.one = one;
303+
this.two = two;
304+
this.three = three;
305+
this.four = four;
306+
}
307+
308+
}
205309
}

0 commit comments

Comments
 (0)