diff --git a/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java b/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java new file mode 100644 index 000000000..c1ea33276 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java @@ -0,0 +1,63 @@ +/* + * Copyright 2011-2024 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.neo4j.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Predicate; + +import org.springframework.core.KotlinDetector; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; + +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +/** + * @author Michael J. Simons + */ +final class KPropertyFilterSupport { + + /** + * Determines all required constructor args for a Kotlin type + * + * @param type the type for which required constructor args must be determined + * @return a list of property names that need to be fetched + */ + static Collection getRequiredProperties(Class type) { + if (!(KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type))) { + return Collections.emptyList(); + } + + PreferredConstructor discover = PreferredConstructorDiscoverer.discover(type); + if (discover == null) { + return Collections.emptyList(); + } + + var preferredConstructor = ReflectJvmMapping.getKotlinFunction(discover.getConstructor()); + if (preferredConstructor == null) { + return Collections.emptyList(); + } + + return preferredConstructor.getParameters().stream() + .filter(Predicate.not(KParameter::isOptional)) + .map(KParameter::getName) + .toList(); + } + + private KPropertyFilterSupport() { + } +} diff --git a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java index 7b6107a92..df3212fba 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java @@ -45,18 +45,25 @@ public final class PropertyFilterSupport { Neo4jMappingContext mappingContext) { ReturnedType returnedType = resultProcessor.getReturnedType(); + Class potentiallyProjectedType = returnedType.getReturnedType(); + Class domainType = returnedType.getDomainType(); + Collection filteredProperties = new HashSet<>(); boolean isProjecting = returnedType.isProjecting(); - boolean isClosedProjection = factory.getProjectionInformation(returnedType.getReturnedType()).isClosed(); + boolean isClosedProjection = factory.getProjectionInformation(potentiallyProjectedType).isClosed(); if (!isProjecting || !isClosedProjection) { return Collections.emptySet(); } for (String inputProperty : returnedType.getInputProperties()) { - addPropertiesFrom(returnedType.getDomainType(), returnedType.getReturnedType(), factory, - filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, returnedType.getReturnedType()).getLeafProperty().getTypeInformation()), mappingContext); + addPropertiesFrom(domainType, potentiallyProjectedType, factory, + filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, potentiallyProjectedType).getLeafProperty().getTypeInformation()), mappingContext); + } + for (String inputProperty : KPropertyFilterSupport.getRequiredProperties(domainType)) { + addPropertiesFrom(domainType, potentiallyProjectedType, factory, + filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, domainType).getLeafProperty().getTypeInformation()), mappingContext); } return filteredProperties; diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt new file mode 100644 index 000000000..8715d4e8a --- /dev/null +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2011-2024 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.neo4j.integration.k + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.neo4j.driver.Driver +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.neo4j.config.AbstractNeo4jConfig +import org.springframework.data.neo4j.core.DatabaseSelectionProvider +import org.springframework.data.neo4j.core.Neo4jTemplate +import org.springframework.data.neo4j.core.schema.Id +import org.springframework.data.neo4j.core.schema.Node +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager +import org.springframework.data.neo4j.repository.Neo4jRepository +import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories +import org.springframework.data.neo4j.test.BookmarkCapture +import org.springframework.data.neo4j.test.Neo4jExtension +import org.springframework.data.neo4j.test.Neo4jIntegrationTest +import org.springframework.stereotype.Repository +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.annotation.EnableTransactionManagement +import org.springframework.transaction.support.TransactionTemplate +import java.util.* + +/** + * @author Michael J. Simons + */ +@Neo4jIntegrationTest +internal class KotlinIssuesIT { + + companion object { + @JvmStatic + private lateinit var neo4jConnectionSupport: Neo4jExtension.Neo4jConnectionSupport + + @BeforeAll + @JvmStatic + fun clearDatabase(@Autowired driver: Driver, @Autowired bookmarkCapture: BookmarkCapture) { + driver.session().use { session -> + session.run("MATCH (n) DETACH DELETE n").consume() + bookmarkCapture.seedWith(session.lastBookmarks()) + } + } + } + + @Test // GH-2899 + fun requiredPropertiesMustBeIncludedInProjections(@Autowired someRepository: KotlinDataClassEntityRepository) { + someRepository.save(KotlinDataClassEntity(propertyOne = "one", propertyTwo = "two")) + val p = someRepository.findAllProjectedBy() + assertThat(p).hasSizeGreaterThan(0) + .first().matches { v -> v.propertyOne == "one" } + } + + @Node + data class KotlinDataClassEntity ( + + @Id + val id: String = UUID.randomUUID().toString(), + val propertyOne: String, + val propertyTwo: String + ) + + interface KotlinDataClassEntityProjection { + val propertyOne: String + } + + @Repository + internal interface KotlinDataClassEntityRepository : Neo4jRepository { + fun findAllProjectedBy(): List + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + open class MyConfig : AbstractNeo4jConfig() { + @Bean + override fun driver(): Driver { + return neo4jConnectionSupport.driver + } + + @Bean + open fun bookmarkCapture(): BookmarkCapture { + return BookmarkCapture() + } + + @Bean + override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager { + val bookmarkCapture = bookmarkCapture() + return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) + } + + @Bean + open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate { + return TransactionTemplate(transactionManager) + } + } +} diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java b/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java new file mode 100644 index 000000000..4111cfbc9 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java @@ -0,0 +1,5 @@ +/** + * Add `k` as package name as I couldn't figure out in which way the presence of KotlinIssuesIT in the split package + * (between the Kotlin and Java code) messed up the application context in tests. + */ +package org.springframework.data.neo4j.integration.k;