Skip to content

Commit 582fd7d

Browse files
GH-2451 - Apply property filter correctly to composite properties too.
The changes defers the decomposition of the property into single values as late as possible, so that property filter can be left unchanged but now works on the actual attribute names. This fixes #2451.
1 parent e6134cf commit 582fd7d

File tree

9 files changed

+294
-9
lines changed

9 files changed

+294
-9
lines changed

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
import org.apiguardian.api.API;
2525
import org.neo4j.cypherdsl.core.Cypher;
26+
import org.neo4j.driver.Value;
27+
import org.springframework.data.neo4j.core.mapping.Constants;
28+
import org.springframework.data.neo4j.core.mapping.MapValueWrapper;
2629
import org.springframework.lang.Nullable;
2730

2831
/**
@@ -53,6 +56,7 @@ void addAll(Map<String, Object> newParameters) {
5356
* @param value The value of the new parameter
5457
* @throws IllegalStateException when a parameter with the given name already exists
5558
*/
59+
@SuppressWarnings("unchecked")
5660
void add(String name, @Nullable Object value) {
5761

5862
if (this.parameters.containsKey(name)) {
@@ -61,7 +65,26 @@ void add(String name, @Nullable Object value) {
6165
"Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'",
6266
name, previousValue == null ? "null" : previousValue.toString(), value == null ? "null" : value.toString()));
6367
}
64-
this.parameters.put(name, value);
68+
69+
if (Constants.NAME_OF_PROPERTIES_PARAM.equals(name) && value != null) {
70+
this.parameters.put(name, unwrapMapValueWrapper((Map<String, Object>) value));
71+
} else {
72+
this.parameters.put(name, value);
73+
}
74+
}
75+
76+
private static Map<String, Object> unwrapMapValueWrapper(Map<String, Object> properties) {
77+
78+
Map<String, Object> newProperties = new HashMap<>(properties.size());
79+
properties.forEach((k, v) -> {
80+
if (v instanceof MapValueWrapper) {
81+
Value mapValue = ((MapValueWrapper) v).getMapValue();
82+
mapValue.keys().forEach(k2 -> newProperties.put(k2, mapValue.get(k2)));
83+
} else {
84+
newProperties.put(k, v);
85+
}
86+
});
87+
return newProperties;
6588
}
6689

6790
/**

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ public void write(Object source, Map<String, Object> parameters) {
195195

196196
final Value value = conversionService.writeValue(propertyAccessor.getProperty(p), p.getTypeInformation(), p.getOptionalConverter());
197197
if (p.isComposite()) {
198-
value.keys().forEach(k -> properties.put(k, value.get(k)));
198+
properties.put(p.getPropertyName(), new MapValueWrapper(value));
199+
//value.keys().forEach(k -> properties.put(k, value.get(k)));
199200
} else {
200201
properties.put(p.getPropertyName(), value);
201202
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2011-2022 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.mapping;
17+
18+
import org.apiguardian.api.API;
19+
import org.neo4j.driver.Value;
20+
21+
/**
22+
* A wrapper or marker for a Neo4j {@link org.neo4j.driver.internal.value.MapValue} that needs to be unwrapped when used
23+
* for properties.
24+
* This class exists solely for projection / filtering purposes: It allows the {@link DefaultNeo4jEntityConverter} to keep
25+
* the composite properties together as long as possible (in the form of above's {@code MapValue}. Thus, the key in the
26+
* {@link Constants#NAME_OF_PROPERTIES_PARAM} fits the filter so that we can continue filtering after binding.
27+
*
28+
* @author Michael J. Simons
29+
*/
30+
@API(status = API.Status.INTERNAL, since = "6.1")
31+
public final class MapValueWrapper {
32+
33+
private final Value mapValue;
34+
35+
MapValueWrapper(Value mapValue) {
36+
this.mapValue = mapValue;
37+
}
38+
39+
public Value getMapValue() {
40+
return mapValue;
41+
}
42+
}

src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919

20+
import java.util.Collections;
21+
2022
import org.junit.jupiter.api.Test;
2123
import org.neo4j.driver.Driver;
2224
import org.neo4j.driver.Record;
@@ -27,6 +29,7 @@
2729
import org.springframework.context.annotation.Configuration;
2830
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
2931
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
32+
import org.springframework.data.neo4j.core.Neo4jTemplate;
3033
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
3134
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
3235
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
@@ -41,8 +44,6 @@
4144
import org.springframework.transaction.PlatformTransactionManager;
4245
import org.springframework.transaction.annotation.EnableTransactionManagement;
4346

44-
import java.util.Collections;
45-
4647
/**
4748
* @author Michael J. Simons
4849
* @soundtrack Die Toten Hosen - Learning English, Lesson Two
@@ -106,6 +107,34 @@ void compositePropertiesOnNodesShouldBeDeleted(@Autowired Repository repository)
106107
}
107108
}
108109

110+
public interface ThingProjection {
111+
112+
ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO();
113+
}
114+
115+
@Test // GH-2451
116+
void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, @Autowired Neo4jTemplate template) {
117+
118+
Long id = createNodeWithCompositeProperties();
119+
ThingWithCompositeProperties thing = repository.findById(id).get();
120+
thing.setDatesWithTransformedKey(Collections.singletonMap("Test", null));
121+
thing.setSomeDatesByEnumA(Collections.singletonMap(ThingWithCompositeProperties.EnumA.VALUE_AA, null));
122+
thing.setSomeOtherDTO(null);
123+
template.saveAs(thing, ThingProjection.class);
124+
125+
try (Session session = driver.session()) {
126+
Record r = session.readTransaction(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t",
127+
Collections.singletonMap("id", id)).single());
128+
Node n = r.get("t").asNode();
129+
assertThat(n.asMap())
130+
.containsKeys(
131+
"someDatesByEnumA.VALUE_AA",
132+
"datesWithTransformedKey.test"
133+
)
134+
.doesNotContainKeys("dto.x", "dto.y", "dto.z");
135+
}
136+
}
137+
109138
public interface Repository extends Neo4jRepository<ThingWithCompositeProperties, Long> {
110139
}
111140

src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCompositePropertiesIT.java

+38
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919

20+
import org.neo4j.driver.Record;
21+
import org.neo4j.driver.Session;
22+
import org.neo4j.driver.types.Node;
2023
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
24+
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
2125
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
2226
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
2327
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
@@ -106,6 +110,40 @@ void compositePropertiesOnNodesShouldBeRead(@Autowired Repository repository) {
106110
.verifyComplete();
107111
}
108112

113+
public interface ThingProjection {
114+
115+
ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO();
116+
}
117+
118+
@Test // GH-2451
119+
void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, @Autowired ReactiveNeo4jTemplate template) {
120+
121+
Long id = createNodeWithCompositeProperties();
122+
repository.findById(id)
123+
.map(thing -> {
124+
thing.setDatesWithTransformedKey(Collections.singletonMap("Test", null));
125+
thing.setSomeDatesByEnumA(Collections.singletonMap(ThingWithCompositeProperties.EnumA.VALUE_AA, null));
126+
thing.setSomeOtherDTO(null);
127+
return thing;
128+
})
129+
.flatMap(thing -> template.saveAs(thing, ThingProjection.class))
130+
.as(StepVerifier::create)
131+
.expectNextCount(1L)
132+
.verifyComplete();
133+
134+
try (Session session = driver.session()) {
135+
Record r = session.readTransaction(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t",
136+
Collections.singletonMap("id", id)).single());
137+
Node n = r.get("t").asNode();
138+
assertThat(n.asMap())
139+
.containsKeys(
140+
"someDatesByEnumA.VALUE_AA",
141+
"datesWithTransformedKey.test"
142+
)
143+
.doesNotContainKeys("dto.x", "dto.y", "dto.z");
144+
}
145+
}
146+
109147
public interface Repository extends ReactiveNeo4jRepository<ThingWithCompositeProperties, Long> {
110148
}
111149

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

+31-5
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919

2020
import java.util.AbstractMap;
21+
import java.util.ArrayList;
2122
import java.util.Collection;
22-
import java.util.Collections;
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.Optional;
@@ -48,13 +48,16 @@
4848
import org.springframework.data.neo4j.core.Neo4jTemplate;
4949
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
5050
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
51+
import org.springframework.data.neo4j.integration.issues.gh2451.WidgetEntity;
52+
import org.springframework.data.neo4j.integration.issues.gh2451.WidgetProjection;
53+
import org.springframework.data.neo4j.integration.issues.gh2451.WidgetRepository;
5154
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;
5455
import org.springframework.data.neo4j.integration.shared.common.NamesOnly;
5556
import org.springframework.data.neo4j.integration.shared.common.NamesOnlyDto;
5657
import org.springframework.data.neo4j.integration.shared.common.NamesWithSpELCity;
5758
import org.springframework.data.neo4j.integration.shared.common.Person;
59+
import org.springframework.data.neo4j.integration.shared.common.PersonDepartmentQueryResult;
60+
import org.springframework.data.neo4j.integration.shared.common.PersonEntity;
5861
import org.springframework.data.neo4j.integration.shared.common.PersonSummary;
5962
import org.springframework.data.neo4j.integration.shared.common.ProjectionTest1O1;
6063
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestLevel1;
@@ -133,6 +136,9 @@ void setup() {
133136
projectionTestRootId = result.get(0).asLong();
134137
projectionTestLevel1Id = result.get(1).asLong();
135138
projectionTest1O1Id = result.get(2).asLong();
139+
140+
transaction.run("create (w:Widget {code: 'Window1', label: 'yyy'})").consume();
141+
136142
transaction.commit();
137143
bookmarkCapture.seedWith(session.lastBookmark());
138144
}
@@ -365,6 +371,22 @@ void projectionsContainingKnownEntitiesShouldWorkFromTemplate(@Autowired Neo4jTe
365371
.satisfies(ProjectionIT::projectedEntities);
366372
}
367373

374+
@Test // GH-2451
375+
void compositePropertiesShouldBeIncludedInProjections(@Autowired WidgetRepository repository,
376+
@Autowired Neo4jTemplate template) {
377+
378+
String code = "Window1";
379+
WidgetEntity window = repository.findByCode(code).get();
380+
window.setLabel("changed");
381+
window.getAdditionalFields().put("key1", "value1");
382+
383+
template.saveAs(window, WidgetProjection.class);
384+
385+
window = repository.findByCode(code).get();
386+
assertThat(window.getLabel()).isEqualTo("changed");
387+
assertThat(window.getAdditionalFields()).containsEntry("key1", "value1");
388+
}
389+
368390
private static void projectedEntities(PersonDepartmentQueryResult personAndDepartment) {
369391
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getId).isEqualTo("p1");
370392
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getEmail).isEqualTo("[email protected]");
@@ -479,7 +501,7 @@ interface PersonRepository extends Neo4jRepository<PersonEntity, String> {
479501
}
480502

481503
@Configuration
482-
@EnableNeo4jRepositories(considerNestedRepositories = true)
504+
@EnableNeo4jRepositories(considerNestedRepositories = true, basePackageClasses = {ProjectionIT.class, WidgetEntity.class})
483505
@EnableTransactionManagement
484506
static class Config extends AbstractNeo4jConfig {
485507

@@ -495,7 +517,11 @@ public BookmarkCapture bookmarkCapture() {
495517

496518
@Override
497519
protected Collection<String> getMappingBasePackages() {
498-
return Collections.singletonList(DepartmentEntity.class.getPackage().getName());
520+
521+
List<String> packages = new ArrayList<>();
522+
packages.add(DepartmentEntity.class.getPackage().getName());
523+
packages.add(WidgetEntity.class.getPackage().getName());
524+
return packages;
499525
}
500526

501527
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2011-2022 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.issues.gh2451;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
22+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
23+
import org.springframework.data.neo4j.core.schema.Id;
24+
import org.springframework.data.neo4j.core.schema.Node;
25+
26+
/**
27+
* @author Michael J. Simons
28+
*/
29+
@Node("Widget")
30+
public class WidgetEntity {
31+
32+
@GeneratedValue
33+
@Id
34+
private Long id;
35+
private String code;
36+
private String label;
37+
38+
@CompositeProperty
39+
private Map<String, String> additionalFields = new HashMap<>();
40+
41+
public Long getId() {
42+
return id;
43+
}
44+
45+
public String getCode() {
46+
return code;
47+
}
48+
49+
public void setCode(String code) {
50+
this.code = code;
51+
}
52+
53+
public String getLabel() {
54+
return label;
55+
}
56+
57+
public void setLabel(String label) {
58+
this.label = label;
59+
}
60+
61+
public Map<String, String> getAdditionalFields() {
62+
return additionalFields;
63+
}
64+
65+
public void setAdditionalFields(Map<String, String> additionalFields) {
66+
this.additionalFields = additionalFields;
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2011-2022 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.issues.gh2451;
17+
18+
import java.util.Map;
19+
20+
/**
21+
* @author Michael J. Simons
22+
*/
23+
public interface WidgetProjection {
24+
25+
String getCode();
26+
27+
String getLabel();
28+
29+
Map<String, Object> getAdditionalFields();
30+
}

0 commit comments

Comments
 (0)