2020import kotlin .reflect .KFunction ;
2121import kotlin .reflect .KParameter ;
2222import kotlin .reflect .KParameter .Kind ;
23+ import kotlin .reflect .KType ;
2324import kotlin .reflect .full .KClasses ;
2425import kotlin .reflect .jvm .ReflectJvmMapping ;
2526
2627import java .lang .reflect .Method ;
2728import java .lang .reflect .Modifier ;
29+ import java .lang .reflect .Type ;
2830import java .util .ArrayList ;
2931import java .util .Arrays ;
3032import java .util .List ;
3739import org .springframework .util .Assert ;
3840
3941/**
40- * Value object to represent a Kotlin {@code copy} method.
42+ * Value object to represent a Kotlin {@code copy} method. The lookup requires a {@code copy} method that matches the
43+ * primary constructor of the class regardless of whether the primary constructor is the persistence constructor.
4144 *
4245 * @author Mark Paluch
4346 * @since 2.1
@@ -59,13 +62,14 @@ private KotlinCopyMethod(Method publicCopyMethod, Method syntheticCopyMethod) {
5962 this .publicCopyMethod = publicCopyMethod ;
6063 this .syntheticCopyMethod = syntheticCopyMethod ;
6164 this .copyFunction = ReflectJvmMapping .getKotlinFunction (publicCopyMethod );
62- this .parameterCount = copyFunction .getParameters ().size ();
65+ this .parameterCount = copyFunction != null ? copyFunction .getParameters ().size () : 0 ;
6366 }
6467
6568 /**
6669 * Attempt to lookup the Kotlin {@code copy} method. Lookup happens in two stages: Find the synthetic copy method and
6770 * then attempt to resolve its public variant.
6871 *
72+ * @param property the property that must be included in the copy method.
6973 * @param type the class.
7074 * @return {@link Optional} {@link KotlinCopyMethod}.
7175 */
@@ -155,7 +159,6 @@ boolean shouldUsePublicCopyMethod(PersistentEntity<?, ?> entity) {
155159 return true ;
156160 }
157161
158- @ SuppressWarnings ("unchecked" )
159162 private static Optional <Method > findPublicCopyMethod (Method defaultKotlinMethod ) {
160163
161164 Class <?> type = defaultKotlinMethod .getDeclaringClass ();
@@ -167,10 +170,7 @@ private static Optional<Method> findPublicCopyMethod(Method defaultKotlinMethod)
167170 return Optional .empty ();
168171 }
169172
170- List <KParameter > constructorArguments = primaryConstructor .getParameters () //
171- .stream () //
172- .filter (it -> it .getKind () == Kind .VALUE ) //
173- .collect (Collectors .toList ());
173+ List <KParameter > constructorArguments = getComponentArguments (primaryConstructor );
174174
175175 return Arrays .stream (type .getDeclaredMethods ()).filter (it -> it .getName ().equals ("copy" ) //
176176 && !it .isSynthetic () //
@@ -207,7 +207,7 @@ private static boolean parameterMatches(List<KParameter> constructorArguments, K
207207
208208 KParameter constructorParameter = constructorArguments .get (constructorArgIndex );
209209
210- if (!constructorParameter .getName ().equals (parameter .getName ())
210+ if (constructorParameter . getName () == null || !constructorParameter .getName ().equals (parameter .getName ())
211211 || !constructorParameter .getType ().equals (parameter .getType ())) {
212212 return false ;
213213 }
@@ -220,14 +220,70 @@ private static boolean parameterMatches(List<KParameter> constructorArguments, K
220220
221221 private static Optional <Method > findSyntheticCopyMethod (Class <?> type ) {
222222
223+ KClass <?> kotlinClass = JvmClassMappingKt .getKotlinClass (type );
224+ KFunction <?> primaryConstructor = KClasses .getPrimaryConstructor (kotlinClass );
225+
226+ if (primaryConstructor == null ) {
227+ return Optional .empty ();
228+ }
229+
223230 return Arrays .stream (type .getDeclaredMethods ()) //
224231 .filter (it -> it .getName ().equals ("copy$default" ) //
225232 && Modifier .isStatic (it .getModifiers ()) //
226233 && it .getReturnType ().equals (type ))
227234 .filter (Method ::isSynthetic ) //
235+ .filter (it -> matchesPrimaryConstructor (it .getParameterTypes (), primaryConstructor ))
228236 .findFirst ();
229237 }
230238
239+ /**
240+ * Verify that the {@code parameterTypes} match arguments of the {@link KFunction primaryConstructor}.
241+ */
242+ private static boolean matchesPrimaryConstructor (Class <?>[] parameterTypes , KFunction <?> primaryConstructor ) {
243+
244+ List <KParameter > constructorArguments = getComponentArguments (primaryConstructor );
245+
246+ int defaultingArgs = KotlinDefaultMask .from (primaryConstructor , kParameter -> false ).getDefaulting ().length ;
247+
248+ if (parameterTypes .length != 1 /* $this */ + constructorArguments .size () + defaultingArgs + 1 /* object marker */ ) {
249+ return false ;
250+ }
251+
252+ // $this comes first
253+ if (!isAssignableFrom (parameterTypes [0 ], primaryConstructor .getReturnType ())) {
254+ return false ;
255+ }
256+
257+ for (int i = 0 ; i < constructorArguments .size (); i ++) {
258+
259+ KParameter kParameter = constructorArguments .get (i );
260+
261+ if (!isAssignableFrom (parameterTypes [i + 1 ], kParameter .getType ())) {
262+ return false ;
263+ }
264+ }
265+
266+ return true ;
267+ }
268+
269+ private static List <KParameter > getComponentArguments (KFunction <?> primaryConstructor ) {
270+ return primaryConstructor .getParameters () //
271+ .stream () //
272+ .filter (it -> it .getKind () == Kind .VALUE ) //
273+ .collect (Collectors .toList ());
274+ }
275+
276+ private static boolean isAssignableFrom (Class <?> target , KType source ) {
277+
278+ Type parameterType = ReflectJvmMapping .getJavaType (source );
279+
280+ if (parameterType instanceof Class ) {
281+ return target .isAssignableFrom ((Class <?>) parameterType );
282+ }
283+
284+ return false ;
285+ }
286+
231287 /**
232288 * Value object to represent Kotlin {@literal copy$default} invocation metadata.
233289 *
0 commit comments