diff --git a/infobip-spring-data-common/pom.xml b/infobip-spring-data-common/pom.xml index 5f6d5126..4d7f0b72 100644 --- a/infobip-spring-data-common/pom.xml +++ b/infobip-spring-data-common/pom.xml @@ -33,5 +33,15 @@ org.springframework.data spring-data-relational + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.junit.jupiter + junit-jupiter + test + diff --git a/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/PreferredConstructorDiscoverer.java b/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/PreferredConstructorDiscoverer.java new file mode 100644 index 00000000..91016052 --- /dev/null +++ b/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/PreferredConstructorDiscoverer.java @@ -0,0 +1,118 @@ +package com.infobip.spring.data.common; + +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KFunction; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.KotlinReflectionUtils; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.*; + +/** + * Utility class to find the preferred constructor which is compatible with both Spring Data JDBC and QueryDSL. + */ +interface PreferredConstructorDiscoverer { + + @Nullable + static > PreferredConstructor discover(Class type) { + return Discoverers.findDiscoverer(type) + .discover(ClassTypeInformation.from(type), null); + } + + enum Discoverers { + + DEFAULT { + + @Nullable + @Override + > PreferredConstructor discover( + TypeInformation type, @Nullable PersistentEntity entity) { + + return Arrays.stream(type.getType().getDeclaredConstructors()) + .filter(it -> !it.isSynthetic()) + .filter(it -> it.isAnnotationPresent(PersistenceConstructor.class)) + .map(it -> buildPreferredConstructor(it, type, entity)) + .findFirst() + .orElseGet(() -> Arrays.stream(type.getType().getDeclaredConstructors()) + .filter(it -> !it.isSynthetic()) + .max(Comparator.comparingInt(Constructor::getParameterCount)) + .map(it -> buildPreferredConstructor(it, type, entity)) + .orElse(null)); + } + }, + + KOTLIN { + + @Nullable + @Override + > PreferredConstructor discover( + TypeInformation type, @Nullable PersistentEntity entity) { + + return Arrays.stream(type.getType().getDeclaredConstructors()) + .filter(it -> !it.isSynthetic()) + .filter(it -> it.isAnnotationPresent(PersistenceConstructor.class)) + .map(it -> buildPreferredConstructor(it, type, entity)) + .findFirst() + .orElseGet(() -> { + KFunction primaryConstructor = KClasses + .getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(type.getType())); + + if (primaryConstructor == null) { + return DEFAULT.discover(type, entity); + } + + Constructor javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); + + return javaConstructor != null ? buildPreferredConstructor(javaConstructor, type, entity) : null; + }); + } + }; + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private static Discoverers findDiscoverer(Class type) { + return KotlinReflectionUtils.isSupportedKotlinClass(type) ? KOTLIN : DEFAULT; + } + + @Nullable + abstract > PreferredConstructor discover(TypeInformation type, + @Nullable PersistentEntity entity); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static > PreferredConstructor buildPreferredConstructor( + Constructor constructor, TypeInformation typeInformation, @Nullable PersistentEntity entity) { + + if (constructor.getParameterCount() == 0) { + return new PreferredConstructor<>((Constructor) constructor); + } + + List> parameterTypes = typeInformation.getParameterTypes(constructor); + String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor); + + PreferredConstructor.Parameter[] parameters = new PreferredConstructor.Parameter[parameterTypes.size()]; + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + + for (int i = 0; i < parameterTypes.size(); i++) { + + String name = parameterNames == null || parameterNames.length <= i ? null : parameterNames[i]; + TypeInformation type = parameterTypes.get(i); + Annotation[] annotations = parameterAnnotations[i]; + + parameters[i] = new PreferredConstructor.Parameter(name, type, annotations, entity); + } + + return new PreferredConstructor<>((Constructor) constructor, parameters); + } + } +} diff --git a/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/QuerydslExpressionFactory.java b/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/QuerydslExpressionFactory.java index 35efc5ad..4847cecd 100644 --- a/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/QuerydslExpressionFactory.java +++ b/infobip-spring-data-common/src/main/java/com/infobip/spring/data/common/QuerydslExpressionFactory.java @@ -6,7 +6,6 @@ import com.querydsl.sql.RelationalPathBase; import org.springframework.core.ResolvableType; import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.util.ReflectionUtils; diff --git a/infobip-spring-data-common/src/test/kotlin/com/infobip/spring/data/common/PreferredConstructorTest.kt b/infobip-spring-data-common/src/test/kotlin/com/infobip/spring/data/common/PreferredConstructorTest.kt new file mode 100644 index 00000000..4a530a71 --- /dev/null +++ b/infobip-spring-data-common/src/test/kotlin/com/infobip/spring/data/common/PreferredConstructorTest.kt @@ -0,0 +1,68 @@ +package com.infobip.spring.data.common + +import org.assertj.core.api.BDDAssertions.then +import org.junit.jupiter.api.Test +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.PersistenceConstructor +import org.springframework.data.mapping.PersistentProperty + +class PreferredConstructorTest { + + @Test + internal fun `it should prefer persistence constructors`() { + val preferredConstructor = PreferredConstructorDiscoverer + .discover>(EntityWithPersistenceConstructor::class.java) + + then(preferredConstructor.parameters).hasSize(3) + then(preferredConstructor.parameters.map { it.name }).containsExactly("id", "firstName", "lastName") + } + + @Test + internal fun `if no persistence constructor, it should take primary constructor`() { + val preferredConstructor = PreferredConstructorDiscoverer + .discover>(DataClass::class.java) + + then(preferredConstructor.parameters).hasSize(3) + then(preferredConstructor.parameters.map { it.name }).containsExactly("id", "firstName", "lastName") + } + + @Test + internal fun `if no primary constructor, it should take constructor with most arguments`() { + val preferredConstructor = PreferredConstructorDiscoverer + .discover>(EntityWithoutPersistenceConstructor::class.java) + + then(preferredConstructor.parameters).hasSize(3) + then(preferredConstructor.parameters.map { it.name }).containsExactly("id", "firstName", "lastName") + } +} + +data class DataClass( + @Id private val id: String, + val firstName: String, + val lastName: String +) + +data class EntityWithPersistenceConstructor( + @Id private val id: String, + private val nameInformation: NameInformation +) { + @PersistenceConstructor + constructor(id: String, firstName: String, lastName: String): this(id, NameInformation(firstName, lastName)) +} + +class EntityWithoutPersistenceConstructor { + val id: String + val nameInformation: NameInformation + + constructor() { + this.id = "_" + this.nameInformation = NameInformation("_", "_") + } + + constructor(id: String, firstName: String, lastName: String) { + this.id = id + this.nameInformation = NameInformation(firstName, lastName) + } +} + +data class NameInformation(val firstName: String, val lastName: String) \ No newline at end of file diff --git a/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/NoArgsEntity.java b/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/NoArgsEntity.java index 000a586a..aa9f5a55 100644 --- a/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/NoArgsEntity.java +++ b/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/NoArgsEntity.java @@ -23,7 +23,6 @@ public class NoArgsEntity { this.value = null; } - @PersistenceConstructor public NoArgsEntity(Long id, String value) { this.id = id; this.value = value; diff --git a/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/QuerydslJdbcRepositoryTest.java b/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/QuerydslJdbcRepositoryTest.java index 98c8c616..001aa46a 100644 --- a/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/QuerydslJdbcRepositoryTest.java +++ b/infobip-spring-data-jdbc-querydsl/src/test/java/com/infobip/spring/data/jdbc/QuerydslJdbcRepositoryTest.java @@ -204,7 +204,7 @@ void shouldBeAbleToJoin() { } @Test - void shouldSupportMultipleConstructors() { + void shouldSupportMultipleConstructorsWithEntityProjection() { // given NoArgsEntity givenNoArgsEntity = giveNoArgsEntity(); @@ -218,6 +218,21 @@ void shouldSupportMultipleConstructors() { then(actual).containsExactly(givenNoArgsEntity); } + @Test + void shouldSupportMultipleConstructors() { + // given + NoArgsEntity givenNoArgsEntity = giveNoArgsEntity(); + + // when + List actual = noArgsRepository.query(query -> query + .select(QNoArgsEntity.noArgsEntity) + .from(QNoArgsEntity.noArgsEntity) + .limit(1) + .fetch()); + + then(actual).containsExactly(givenNoArgsEntity); + } + @Test void shouldExtendSimpleQuerydslJdbcRepository() { // then diff --git a/pom.xml b/pom.xml index 5b59fa91..0adeb284 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,8 @@ UTF-8 1.8 + + 1.5.30 @@ -159,6 +161,11 @@ ${infobip-mssql-testcontainers.version} test + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + @@ -185,10 +192,57 @@ + + kotlin-maven-plugin + + 1.8 + + org.jetbrains.kotlin + ${kotlin.version} + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + ${java.version} ${java.version}