Skip to content

Commit 79ab5ad

Browse files
Fix potential NPE in TupleConverter
1 parent 4c1c3e8 commit 79ab5ad

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

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

+48-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import jakarta.persistence.TupleElement;
2424
import jakarta.persistence.TypedQuery;
2525

26+
import java.lang.reflect.Constructor;
2627
import java.util.Arrays;
2728
import java.util.Collection;
2829
import java.util.HashMap;
@@ -54,6 +55,7 @@
5455
import org.springframework.jdbc.support.JdbcUtils;
5556
import org.springframework.lang.Nullable;
5657
import org.springframework.util.Assert;
58+
import org.springframework.util.ClassUtils;
5759
import org.springframework.util.ReflectionUtils;
5860

5961
/**
@@ -345,7 +347,7 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) {
345347
&& !type.getInputProperties().isEmpty();
346348

347349
if (this.dtoProjection) {
348-
this.preferredConstructor = PreferredConstructorDiscoverer.discover(String.class);
350+
this.preferredConstructor = PreferredConstructorDiscoverer.discover(type.getReturnedType());
349351
} else {
350352
this.preferredConstructor = null;
351353
}
@@ -373,18 +375,35 @@ public Object convert(Object source) {
373375

374376
Object[] ctorArgs = new Object[type.getInputProperties().size()];
375377

378+
boolean containsNullValue = false;
376379
for (int i = 0; i < type.getInputProperties().size(); i++) {
377-
ctorArgs[i] = tuple.get(i);
380+
Object value = tuple.get(i);
381+
ctorArgs[i] = value;
382+
if (!containsNullValue && value == null) {
383+
containsNullValue = true;
384+
}
378385
}
379386

380387
try {
381388

382-
if (preferredConstructor.getParameterCount() == ctorArgs.length) {
389+
if (preferredConstructor != null && preferredConstructor.getParameterCount() == ctorArgs.length) {
383390
return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs);
384391
}
385392

386-
return BeanUtils.instantiateClass(type.getReturnedType()
387-
.getConstructor(Arrays.stream(ctorArgs).map(Object::getClass).toArray(Class<?>[]::new)), ctorArgs);
393+
Constructor<?> ctor = null;
394+
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+
}
399+
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+
}
403+
404+
if (ctor != null) {
405+
return BeanUtils.instantiateClass(ctor, ctorArgs);
406+
}
388407
} catch (ReflectiveOperationException e) {
389408
ReflectionUtils.handleReflectionException(e);
390409
}
@@ -393,6 +412,30 @@ public Object convert(Object source) {
393412
return new TupleBackedMap(tupleWrapper.apply(tuple));
394413
}
395414

415+
@Nullable
416+
private Constructor<?> findFirstMatchingConstructor(Object[] ctorArgs) {
417+
418+
for (Constructor<?> ctor : type.getReturnedType().getDeclaredConstructors()) {
419+
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+
}
434+
}
435+
}
436+
return null;
437+
}
438+
396439
/**
397440
* A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
398441
* {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the

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

+25
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
import org.springframework.data.jpa.repository.query.AbstractJpaQuery.TupleConverter;
3939
import org.springframework.data.projection.ProjectionFactory;
40+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
4041
import org.springframework.data.repository.CrudRepository;
4142
import org.springframework.data.repository.core.RepositoryMetadata;
4243
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@@ -113,6 +114,16 @@ void findsValuesForAllVariantsSupportedByTheTuple() {
113114
softly.assertAll();
114115
}
115116

117+
@Test // GH-3076
118+
void dealsWithNullsInArgumetents() {
119+
120+
ReturnedType returnedType = ReturnedType.of(WithPC.class, DomainType.class, new SpelAwareProxyProjectionFactory());
121+
122+
when(tuple.get(eq(0))).thenReturn("one");
123+
when(tuple.get(eq(1))).thenReturn(null);
124+
new TupleConverter(returnedType).convert(tuple);
125+
}
126+
116127
interface SampleRepository extends CrudRepository<Object, Long> {
117128
String someMethod();
118129
}
@@ -177,4 +188,18 @@ public String getAlias() {
177188
}
178189
}
179190
}
191+
192+
static class DomainType {
193+
String one, two, three;
194+
}
195+
196+
static class WithPC {
197+
String one;
198+
String two;
199+
200+
public WithPC(String one, String two) {
201+
this.one = one;
202+
this.two = two;
203+
}
204+
}
180205
}

0 commit comments

Comments
 (0)