Skip to content

Commit 6aace2e

Browse files
fix: Load properties required to instantiate KClasses backing projections proper.
Closes #2899
1 parent 672e02e commit 6aace2e

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2011-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core;
17+
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.function.Predicate;
21+
22+
import org.springframework.core.KotlinDetector;
23+
import org.springframework.data.mapping.PreferredConstructor;
24+
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
25+
26+
import kotlin.reflect.KParameter;
27+
import kotlin.reflect.jvm.ReflectJvmMapping;
28+
29+
/**
30+
* @author Michael J. Simons
31+
*/
32+
final class KPropertyFilterSupport {
33+
34+
/**
35+
* Determines all required constructor args for a Kotlin type
36+
*
37+
* @param type the type for which required constructor args must be determined
38+
* @return a list of property names that need to be fetched
39+
*/
40+
static Collection<String> getRequiredProperties(Class<?> type) {
41+
if (!(KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type))) {
42+
return Collections.emptyList();
43+
}
44+
45+
PreferredConstructor<?, ?> discover = PreferredConstructorDiscoverer.discover(type);
46+
if (discover == null) {
47+
return Collections.emptyList();
48+
}
49+
50+
var preferredConstructor = ReflectJvmMapping.getKotlinFunction(discover.getConstructor());
51+
if (preferredConstructor == null) {
52+
return Collections.emptyList();
53+
}
54+
55+
return preferredConstructor.getParameters().stream()
56+
.filter(Predicate.not(KParameter::isOptional))
57+
.map(KParameter::getName)
58+
.toList();
59+
}
60+
61+
private KPropertyFilterSupport() {
62+
}
63+
}

src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,25 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
4545
Neo4jMappingContext mappingContext) {
4646

4747
ReturnedType returnedType = resultProcessor.getReturnedType();
48+
Class<?> potentiallyProjectedType = returnedType.getReturnedType();
49+
Class<?> domainType = returnedType.getDomainType();
50+
4851
Collection<PropertyFilter.ProjectedPath> filteredProperties = new HashSet<>();
4952

5053
boolean isProjecting = returnedType.isProjecting();
51-
boolean isClosedProjection = factory.getProjectionInformation(returnedType.getReturnedType()).isClosed();
54+
boolean isClosedProjection = factory.getProjectionInformation(potentiallyProjectedType).isClosed();
5255

5356
if (!isProjecting || !isClosedProjection) {
5457
return Collections.emptySet();
5558
}
5659

5760
for (String inputProperty : returnedType.getInputProperties()) {
58-
addPropertiesFrom(returnedType.getDomainType(), returnedType.getReturnedType(), factory,
59-
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, returnedType.getReturnedType()).getLeafProperty().getTypeInformation()), mappingContext);
61+
addPropertiesFrom(domainType, potentiallyProjectedType, factory,
62+
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, potentiallyProjectedType).getLeafProperty().getTypeInformation()), mappingContext);
63+
}
64+
for (String inputProperty : KPropertyFilterSupport.getRequiredProperties(domainType)) {
65+
addPropertiesFrom(domainType, potentiallyProjectedType, factory,
66+
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, domainType).getLeafProperty().getTypeInformation()), mappingContext);
6067
}
6168

6269
return filteredProperties;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2011-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.neo4j.integration.k
18+
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.jupiter.api.BeforeAll
21+
import org.junit.jupiter.api.Test
22+
import org.neo4j.driver.Driver
23+
import org.springframework.beans.factory.annotation.Autowired
24+
import org.springframework.context.annotation.Bean
25+
import org.springframework.context.annotation.Configuration
26+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig
27+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider
28+
import org.springframework.data.neo4j.core.Neo4jTemplate
29+
import org.springframework.data.neo4j.core.schema.Id
30+
import org.springframework.data.neo4j.core.schema.Node
31+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager
32+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager
33+
import org.springframework.data.neo4j.repository.Neo4jRepository
34+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories
35+
import org.springframework.data.neo4j.test.BookmarkCapture
36+
import org.springframework.data.neo4j.test.Neo4jExtension
37+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest
38+
import org.springframework.stereotype.Repository
39+
import org.springframework.transaction.PlatformTransactionManager
40+
import org.springframework.transaction.annotation.EnableTransactionManagement
41+
import org.springframework.transaction.support.TransactionTemplate
42+
import java.util.*
43+
44+
/**
45+
* @author Michael J. Simons
46+
*/
47+
@Neo4jIntegrationTest
48+
internal class KotlinIssuesIT {
49+
50+
companion object {
51+
@JvmStatic
52+
private lateinit var neo4jConnectionSupport: Neo4jExtension.Neo4jConnectionSupport
53+
54+
@BeforeAll
55+
@JvmStatic
56+
fun clearDatabase(@Autowired driver: Driver, @Autowired bookmarkCapture: BookmarkCapture) {
57+
driver.session().use { session ->
58+
session.run("MATCH (n) DETACH DELETE n").consume()
59+
bookmarkCapture.seedWith(session.lastBookmarks())
60+
}
61+
}
62+
}
63+
64+
@Test // GH-2899
65+
fun requiredPropertiesMustBeIncludedInProjections(@Autowired someRepository: KotlinDataClassEntityRepository) {
66+
someRepository.save(KotlinDataClassEntity(propertyOne = "one", propertyTwo = "two"))
67+
val p = someRepository.findAllProjectedBy()
68+
assertThat(p).hasSizeGreaterThan(0)
69+
.first().matches { v -> v.propertyOne == "one" }
70+
}
71+
72+
@Node
73+
data class KotlinDataClassEntity (
74+
75+
@Id
76+
val id: String = UUID.randomUUID().toString(),
77+
val propertyOne: String,
78+
val propertyTwo: String
79+
)
80+
81+
interface KotlinDataClassEntityProjection {
82+
val propertyOne: String
83+
}
84+
85+
@Repository
86+
internal interface KotlinDataClassEntityRepository : Neo4jRepository<KotlinDataClassEntity, String> {
87+
fun findAllProjectedBy(): List<KotlinDataClassEntityProjection>
88+
}
89+
90+
@Configuration
91+
@EnableTransactionManagement
92+
@EnableNeo4jRepositories(considerNestedRepositories = true)
93+
open class MyConfig : AbstractNeo4jConfig() {
94+
@Bean
95+
override fun driver(): Driver {
96+
return neo4jConnectionSupport.driver
97+
}
98+
99+
@Bean
100+
open fun bookmarkCapture(): BookmarkCapture {
101+
return BookmarkCapture()
102+
}
103+
104+
@Bean
105+
override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager {
106+
val bookmarkCapture = bookmarkCapture()
107+
return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture))
108+
}
109+
110+
@Bean
111+
open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate {
112+
return TransactionTemplate(transactionManager)
113+
}
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Add `k` as package name as I couldn't figure out in which way the presence of KotlinIssuesIT in the split package
3+
* (between the Kotlin and Java code) messed up the application context in tests.
4+
*/
5+
package org.springframework.data.neo4j.integration.k;

0 commit comments

Comments
 (0)