2424import jakarta .persistence .TypedQuery ;
2525
2626import java .lang .reflect .Constructor ;
27+ import java .util .ArrayList ;
2728import java .util .Arrays ;
2829import java .util .Collection ;
2930import java .util .HashMap ;
3435import java .util .stream .Collectors ;
3536
3637import org .springframework .beans .BeanUtils ;
38+ import org .springframework .core .MethodParameter ;
3739import org .springframework .core .convert .converter .Converter ;
3840import org .springframework .data .jpa .provider .PersistenceProvider ;
3941import org .springframework .data .jpa .repository .EntityGraph ;
5658import org .springframework .lang .Nullable ;
5759import org .springframework .util .Assert ;
5860import 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 /**
0 commit comments