diff --git a/pom.xml b/pom.xml index 13bf0377b4..df0bcba03c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.1.0-SNAPSHOT + 3.1.x-GH-1947-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/mapping/Parameter.java b/src/main/java/org/springframework/data/mapping/Parameter.java index 0f067a3382..8bc716216c 100644 --- a/src/main/java/org/springframework/data/mapping/Parameter.java +++ b/src/main/java/org/springframework/data/mapping/Parameter.java @@ -42,6 +42,7 @@ public class Parameter> { private final Lazy enclosingClassCache; private final Lazy hasSpelExpression; + private boolean isNullableMarker; /** * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of @@ -55,13 +56,28 @@ public class Parameter> { */ public Parameter(@Nullable String name, TypeInformation type, Annotation[] annotations, @Nullable PersistentEntity entity) { + this(name, type, MergedAnnotations.from(annotations), entity, false); + } + + /** + * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of + * {@link Annotation}s. Will inspect the annotations for an {@link Value} annotation to lookup a key or an SpEL + * expression to be evaluated. + * + * @param name the name of the parameter, can be {@literal null} + * @param type must not be {@literal null} + * @param annotations must not be {@literal null} but can be empty + * @param entity must not be {@literal null}. + */ + private Parameter(@Nullable String name, TypeInformation type, MergedAnnotations annotations, + @Nullable PersistentEntity entity, boolean isNullableMarker) { Assert.notNull(type, "Type must not be null"); Assert.notNull(annotations, "Annotations must not be null"); this.name = name; this.type = type; - this.annotations = MergedAnnotations.from(annotations); + this.annotations = annotations; this.key = getValue(this.annotations); this.entity = entity; @@ -75,7 +91,8 @@ public Parameter(@Nullable String name, TypeInformation type, Annotation[] an return owningType.isMemberClass() && type.getType().equals(owningType.getEnclosingClass()); }); - this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression())); + this.hasSpelExpression = isNullableMarker ? Lazy.of(false) : Lazy.of(() -> StringUtils.hasText(getSpelExpression())); + this.isNullableMarker = isNullableMarker; } @Nullable @@ -87,6 +104,10 @@ private static String getValue(MergedAnnotations annotations) { .orElse(null); } + public Parameter nullableMarker() { + return new Parameter(name, type, annotations, entity, true); + } + /** * Returns the name of the parameter. * @@ -143,6 +164,10 @@ public boolean hasSpelExpression() { return this.hasSpelExpression.get(); } + public boolean isNullableMarker() { + return isNullableMarker; + } + @Override public boolean equals(Object o) { diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java index 267846cb64..003a024efb 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java @@ -298,7 +298,7 @@ static

, T> Object[] extractInvocationArguments( int index = 0; for (Parameter parameter : constructor.getParameters()) { - params[index++] = provider.getParameterValue(parameter); + params[index++] = !parameter.isNullableMarker() ? provider.getParameterValue(parameter) : null; } return params; diff --git a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java index 73e99d225b..9b43c476fc 100644 --- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java @@ -16,6 +16,7 @@ package org.springframework.data.mapping.model; import kotlin.jvm.JvmClassMappingKt; +import kotlin.jvm.internal.DefaultConstructorMarker; import kotlin.reflect.KFunction; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.ReflectJvmMapping; @@ -195,10 +196,24 @@ > PreferredConstructor discover(TypeInf } Constructor javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); - return javaConstructor != null ? buildPreferredConstructor(javaConstructor, type, entity) : null; }); } + + @Override + > Parameter buildParameter(PersistentEntity owingEntity, String name, + TypeInformation type, Annotation[] annotations) { + + if (name != null) { + return super.buildParameter(owingEntity, name, type, annotations); + } + + if (ClassUtils.isAssignable(DefaultConstructorMarker.class, type.getType())) { + return super.buildParameter(owingEntity, "$defaultConstructorMarker", type, annotations).nullableMarker(); + } + + throw new IllegalStateException("oh no - cannot build parameter for " + name); + } }; private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); @@ -224,8 +239,13 @@ private static Discoverers findDiscoverer(Class type) { abstract > PreferredConstructor discover(TypeInformation type, @Nullable PersistentEntity entity); + > Parameter buildParameter(PersistentEntity owingEntity, String name, + TypeInformation type, Annotation[] annotations) { + return new Parameter(name, type, annotations, owingEntity); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) - private static > PreferredConstructor buildPreferredConstructor( + > PreferredConstructor buildPreferredConstructor( Constructor constructor, TypeInformation typeInformation, @Nullable PersistentEntity entity) { if (constructor.getParameterCount() == 0) { @@ -244,7 +264,7 @@ private static > PreferredConstructor b TypeInformation type = parameterTypes.get(i); Annotation[] annotations = parameterAnnotations[i]; - parameters[i] = new Parameter(name, type, annotations, entity); + parameters[i] = buildParameter(entity, name, type, annotations); } return new PreferredConstructor<>((Constructor) constructor, parameters); diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java index e522eef3ce..1f33094dfb 100755 --- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java @@ -439,6 +439,33 @@ void entityInstantiatorShouldFailForAbstractClass() { .createInstance(new BasicPersistentEntity<>(TypeInformation.of(AbstractDto.class)), provider)); } + @Test // GH-1947 + void skipsValueRetrievalForKotlinDefaultConstructorMarker() { + + doReturn(TheTypeConsumingAnInlineClass.class).when(entity).getType(); + doReturn(PreferredConstructorDiscoverer.discover(TheTypeConsumingAnInlineClass.class))// + .when(entity).getInstanceCreatorMetadata(); + + ParameterValueProvider

valueProvider = new ParameterValueProvider<>() { + + public Object getParameterValue(Parameter parameter) { + + if (parameter.getName() == null || "$defaultConstructorMarker".equals(parameter.getName())) { + throw new RuntimeException("Stop, Hammer time! You can't touch this! Break id down!"); + } + return parameter.getName(); + } + }; + + Object instance = new ClassGeneratingEntityInstantiator().createInstance(entity, valueProvider); + assertThat(instance).isInstanceOf(TheTypeConsumingAnInlineClass.class).satisfies(it -> { + assertThat(it.toString()) // + .contains("inlineTypeParam=TheActualInlineClass(id=inlineTypeParam)") // + .contains("someStringParam=someStringParam") // + .contains("id=id"); + }); + } + private void prepareMocks(Class type) { doReturn(type).when(entity).getType(); diff --git a/src/test/kotlin/org/springframework/data/mapping/model/WithInlineClass.kt b/src/test/kotlin/org/springframework/data/mapping/model/WithInlineClass.kt new file mode 100644 index 0000000000..4b6783aba0 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/model/WithInlineClass.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model + +inline class TheActualInlineClass(val id: String) +data class TheTypeConsumingAnInlineClass(val inlineTypeParam: TheActualInlineClass, val someStringParam: String, val id: String) +