Skip to content

Commit 0ec3b9c

Browse files
GH-2349 - Fix NullPointerException when using known entities inside projections.
When a known entity is discovered in the PropertyFilterSupport, it can be used directly as the property type than is equal to a domain type (a well known entity). Prior to that change, the discovered well known entity was used and it was asked for the the type of the property of the containing class again, which is wrong in most cases that don't have a property of the fitting type _and_ the same name again. In addition, one other bug has been fixed: The `Neo4jTemplate` did not pass result type and domain type in the `saveAs` method to the `PropertyFilterSupport`, but only the result type, leading to the original fix to blow up. Having fixed that bug, it became appearent that in the `PropertyFilterSupport` the overloads of `addPropertiesFrom` different order of `domainType` and `returnType`which then has been fixed as well. This closes #2349.
1 parent 41b90aa commit 0ec3b9c

File tree

8 files changed

+314
-10
lines changed

8 files changed

+314
-10
lines changed

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
332332
}
333333

334334
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
335-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, resultType,
335+
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
336336
projectionFactory, neo4jMappingContext);
337337

338338
T savedInstance = saveImpl(instance, pps);
@@ -496,7 +496,7 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
496496

497497
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
498498

499-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, commonElementType,
499+
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
500500
projectionFactory, neo4jMappingContext);
501501

502502
List<T> savedInstances = saveAllImpl(instances, new ArrayList<>(pps));
@@ -898,7 +898,7 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
898898

899899
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
900900

901-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, domainType,
901+
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
902902
projectionFactory, neo4jMappingContext);
903903

904904
List<R> results = new ArrayList<>();

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.neo4j.core;
1717

18+
import org.apiguardian.api.API;
1819
import org.springframework.data.mapping.PropertyPath;
1920
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
2021
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
@@ -35,7 +36,8 @@
3536
* This class is responsible for creating a List of {@link PropertyPath} entries that contains all reachable
3637
* properties (w/o circles).
3738
*/
38-
public class PropertyFilterSupport {
39+
@API(status = API.Status.INTERNAL, since = "6.1.3")
40+
public final class PropertyFilterSupport {
3941

4042
public static List<PropertyPath> getInputProperties(ResultProcessor resultProcessor, ProjectionFactory factory,
4143
Neo4jMappingContext mappingContext) {
@@ -58,14 +60,14 @@ public static List<PropertyPath> getInputProperties(ResultProcessor resultProces
5860
return filteredProperties;
5961
}
6062

61-
public static List<PropertyPath> addPropertiesFrom(Class<?> returnType, Class<?> domainType,
63+
static List<PropertyPath> addPropertiesFrom(Class<?> domainType, Class<?> returnType,
6264
ProjectionFactory projectionFactory,
6365
Neo4jMappingContext neo4jMappingContext) {
6466

6567
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
6668
List<PropertyPath> propertyPaths = new ArrayList<>();
6769
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
68-
addPropertiesFrom(returnType, domainType, projectionFactory, propertyPaths, inputProperty.getName(), neo4jMappingContext);
70+
addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, inputProperty.getName(), neo4jMappingContext);
6971
}
7072
return propertyPaths;
7173
}
@@ -97,8 +99,8 @@ private static void addPropertiesFrom(Class<?> domainType, Class<?> returnedType
9799
if (propertyType.equals(domainType)) {
98100
return;
99101
}
100-
processEntity(domainType, filteredProperties, inputProperty, mappingContext);
101102

103+
addPropertiesFromEntity(filteredProperties, propertyPath, propertyType, mappingContext, new HashSet<>());
102104
} else {
103105
ProjectionInformation nestedProjectionInformation = factory.getProjectionInformation(propertyType);
104106
filteredProperties.add(propertyPath);

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ public <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {
316316
}
317317

318318
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
319-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, instance.getClass(),
319+
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
320320
projectionFactory, neo4jMappingContext);
321321

322322
Mono<T> savingPublisher = saveImpl(instance, pps);
@@ -342,7 +342,7 @@ <T, R> Flux<R> doSave(Iterable<R> instances, Class<T> domainType) {
342342

343343
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
344344

345-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, domainType,
345+
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
346346
projectionFactory, neo4jMappingContext);
347347

348348
return Flux.fromIterable(instances)
@@ -443,7 +443,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
443443
}
444444

445445
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
446-
List<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(resultType, commonElementType,
446+
List<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
447447
projectionFactory, neo4jMappingContext);
448448

449449
Flux<T> savedInstances = saveAllImpl(instances, pps);

src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java

+45
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.util.AbstractMap;
2121
import java.util.Collection;
22+
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
@@ -44,8 +45,12 @@
4445
import org.springframework.data.domain.Sort;
4546
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
4647
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
48+
import org.springframework.data.neo4j.core.Neo4jTemplate;
4749
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
4850
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
51+
import org.springframework.data.neo4j.integration.shared.common.DepartmentEntity;
52+
import org.springframework.data.neo4j.integration.shared.common.PersonDepartmentQueryResult;
53+
import org.springframework.data.neo4j.integration.shared.common.PersonEntity;
4954
import org.springframework.data.neo4j.integration.shared.common.NamesOnly;
5055
import org.springframework.data.neo4j.integration.shared.common.NamesOnlyDto;
5156
import org.springframework.data.neo4j.integration.shared.common.NamesWithSpELCity;
@@ -98,6 +103,7 @@ void setup() {
98103
Transaction transaction = session.beginTransaction();) {
99104

100105
transaction.run("MATCH (n) detach delete n");
106+
transaction.run("CREATE (p:PersonEntity {id: 'p1', email: '[email protected]'}) -[:MEMBER_OF]->(department:DepartmentEntity {id: 'd1', name: 'Dep1'}) RETURN p");
101107

102108
for (Map.Entry<String, String> person : new Map.Entry[] {
103109
new AbstractMap.SimpleEntry(FIRST_NAME, LAST_NAME),
@@ -337,6 +343,35 @@ void findCypherDSLClosedProjection(@Autowired ProjectionPersonRepository reposit
337343
assertThat(personSummary.get().getLastName()).isEqualTo(LAST_NAME);
338344
}
339345

346+
@Test // GH-2349
347+
void projectionsContainingKnownEntitiesShouldWorkFromRepository(@Autowired PersonRepository personRepository) {
348+
349+
List<PersonDepartmentQueryResult> results = personRepository.findPersonWithDepartment();
350+
assertThat(results)
351+
.hasSize(1)
352+
.first()
353+
.satisfies(personAndDepartment -> projectedEntities(personAndDepartment));
354+
}
355+
356+
@Test // GH-2349
357+
void projectionsContainingKnownEntitiesShouldWorkFromTemplate(@Autowired Neo4jTemplate template) {
358+
359+
List<PersonDepartmentQueryResult> results = template.find(PersonEntity.class).as(PersonDepartmentQueryResult.class)
360+
.matching("MATCH (person:PersonEntity)-[:MEMBER_OF]->(department:DepartmentEntity) RETURN person, department")
361+
.all();
362+
assertThat(results)
363+
.hasSize(1)
364+
.first()
365+
.satisfies(personAndDepartment -> projectedEntities(personAndDepartment));
366+
}
367+
368+
private static void projectedEntities(PersonDepartmentQueryResult personAndDepartment) {
369+
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getId).isEqualTo("p1");
370+
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getEmail).isEqualTo("[email protected]");
371+
assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getId).isEqualTo("d1");
372+
assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getName).isEqualTo("Dep1");
373+
}
374+
340375
private static Statement whoHasFirstName(String firstName) {
341376
Node p = Cypher.node("Person").named("p");
342377
return Cypher.match(p)
@@ -438,6 +473,11 @@ interface Subprojection2 {
438473
}
439474
}
440475

476+
interface PersonRepository extends Neo4jRepository<PersonEntity, String> {
477+
@Query("MATCH (person:PersonEntity)-[:MEMBER_OF]->(department:DepartmentEntity) RETURN person, department")
478+
List<PersonDepartmentQueryResult> findPersonWithDepartment();
479+
}
480+
441481
@Configuration
442482
@EnableNeo4jRepositories(considerNestedRepositories = true)
443483
@EnableTransactionManagement
@@ -453,6 +493,11 @@ public BookmarkCapture bookmarkCapture() {
453493
return new BookmarkCapture();
454494
}
455495

496+
@Override
497+
protected Collection<String> getMappingBasePackages() {
498+
return Collections.singletonList(DepartmentEntity.class.getPackage().getName());
499+
}
500+
456501
@Override
457502
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) {
458503

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2011-2021 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.integration.shared.common;
17+
18+
import org.springframework.data.neo4j.core.schema.Id;
19+
import org.springframework.data.neo4j.core.schema.Node;
20+
21+
/**
22+
* @∆author Michael J. Simons
23+
*/
24+
@Node
25+
public class DepartmentEntity {
26+
27+
@Id
28+
private final String id;
29+
private final String name;
30+
31+
public DepartmentEntity(String id, String name) {
32+
this.id = id;
33+
this.name = name;
34+
}
35+
36+
public String getId() {
37+
return this.id;
38+
}
39+
40+
public String getName() {
41+
return this.name;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2011-2021 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.integration.shared.common;
17+
18+
/**
19+
* @author Michael J. Simons
20+
*/
21+
public class PersonDepartmentQueryResult {
22+
23+
private final PersonEntity person;
24+
private final DepartmentEntity department;
25+
26+
public PersonDepartmentQueryResult(PersonEntity person, DepartmentEntity department) {
27+
this.person = person;
28+
this.department = department;
29+
}
30+
31+
public PersonEntity getPerson() {
32+
return this.person;
33+
}
34+
35+
public DepartmentEntity getDepartment() {
36+
return this.department;
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2011-2021 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.integration.shared.common;
17+
18+
import org.springframework.data.neo4j.core.schema.Id;
19+
import org.springframework.data.neo4j.core.schema.Node;
20+
21+
/**
22+
* @author Michael J. Simons
23+
*/
24+
@Node
25+
public class PersonEntity {
26+
@Id
27+
private final String id;
28+
private final String email;
29+
30+
public PersonEntity(String id, String email) {
31+
this.id = id;
32+
this.email = email;
33+
}
34+
35+
public String getId() {
36+
return this.id;
37+
}
38+
39+
public String getEmail() {
40+
return this.email;
41+
}
42+
}

0 commit comments

Comments
 (0)