Skip to content

Commit e0074f1

Browse files
GH-2474 - Allow escaped properties in custom sort order.
Fixes #2474.
1 parent 860fc34 commit e0074f1

File tree

7 files changed

+327
-6
lines changed

7 files changed

+327
-6
lines changed

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,18 @@ public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntit
482482
if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) {
483483
expression = Cypher.raw(property);
484484
} else if (property.contains(".")) {
485-
String[] path = property.split("\\.");
486-
if (path.length != 2) {
487-
throw new IllegalArgumentException(String.format(
488-
"Cannot handle order property `%s`, it must be a simple property or one-hop path.",
489-
property));
485+
int firstDot = property.indexOf('.');
486+
String tail = property.substring(firstDot + 1);
487+
if (tail.isEmpty() || property.lastIndexOf(".") != firstDot) {
488+
if (tail.trim().matches("`.+`")) {
489+
tail = tail.replaceFirst("`(.+)`", "$1");
490+
} else {
491+
throw new IllegalArgumentException(String.format(
492+
"Cannot handle order property `%s`, it must be a simple property or one-hop path.",
493+
property));
494+
}
490495
}
491-
expression = Cypher.property(path[0], path[1]);
496+
expression = Cypher.property(property.substring(0, firstDot), tail);
492497
} else {
493498
expression = Cypher.name(property);
494499
}

src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ void shouldFailOnInvalidPathWithMultipleHops() {
176176
.withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path\\.");
177177
}
178178

179+
@Test // GH-2474
180+
void shouldNotFailOnMultipleEscapedHops() {
181+
182+
Optional<String> fragment = Optional.ofNullable(CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n.`a.b.c`")));
183+
assertThat(fragment).hasValue("ORDER BY n.`a.b.c` ASC");
184+
}
185+
179186
@CsvSource(delimiterString = "|", value = {
180187
"apoc.text.clean(department.name) |false| ORDER BY apoc.text.clean(department.name) ASC",
181188
"apoc.text.clean(department.name) |true | ORDER BY apoc.text.clean(department.name) DESC",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.gh2474;
17+
18+
import lombok.Data;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.UUID;
23+
24+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
25+
import org.springframework.data.neo4j.core.schema.Id;
26+
import org.springframework.data.neo4j.core.schema.Node;
27+
import org.springframework.data.neo4j.core.schema.Property;
28+
import org.springframework.data.neo4j.core.schema.Relationship;
29+
30+
/**
31+
* @author Stephen Jackson
32+
*/
33+
@Node
34+
@Data
35+
public class CityModel {
36+
@Id
37+
@GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class)
38+
private UUID cityId;
39+
40+
@Relationship(value = "MAYOR")
41+
private PersonModel mayor;
42+
43+
@Relationship(value = "CITIZEN")
44+
private List<PersonModel> citizens = new ArrayList<>();
45+
46+
@Relationship(value = "EMPLOYEE")
47+
private List<JobRelationship> cityEmployees = new ArrayList<>();
48+
49+
private String name;
50+
51+
@Property("exotic.property")
52+
private String exoticProperty;
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.gh2474;
17+
18+
import java.util.List;
19+
import java.util.UUID;
20+
21+
import org.springframework.data.domain.Sort;
22+
import org.springframework.data.neo4j.repository.Neo4jRepository;
23+
import org.springframework.data.neo4j.repository.query.Query;
24+
25+
/**
26+
* @author Stephen Jackson
27+
* @author Michael J. Simons
28+
*/
29+
public interface CityModelRepository extends Neo4jRepository<CityModel, UUID> {
30+
31+
@Query(""
32+
+ "MATCH (n:CityModel)"
33+
+ "RETURN n :#{orderBy(#sort)}")
34+
List<CityModel> customQuery(Sort sort);
35+
36+
long deleteAllByExoticProperty(String property);
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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.gh2474;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.neo4j.driver.Driver;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.data.domain.Sort;
30+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
31+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
32+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
33+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
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.transaction.PlatformTransactionManager;
39+
import org.springframework.transaction.annotation.EnableTransactionManagement;
40+
41+
/**
42+
* @author Stephen Jackson
43+
* @author Michael J. Simons
44+
*/
45+
@Neo4jIntegrationTest
46+
public class GH2474IT {
47+
48+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
49+
50+
@Autowired
51+
Driver driver;
52+
53+
@Autowired
54+
BookmarkCapture bookmarkCapture;
55+
56+
@Autowired
57+
CityModelRepository cityModelRepository;
58+
59+
@BeforeEach
60+
void setupData() {
61+
62+
cityModelRepository.deleteAll();
63+
64+
CityModel aachen = new CityModel();
65+
aachen.setName("Aachen");
66+
aachen.setExoticProperty("Cars");
67+
68+
CityModel utrecht = new CityModel();
69+
utrecht.setName("Utrecht");
70+
utrecht.setExoticProperty("Bikes");
71+
72+
cityModelRepository.saveAll(Arrays.asList(aachen, utrecht));
73+
}
74+
75+
@Test
76+
public void testStoreExoticProperty() {
77+
78+
CityModel cityModel = new CityModel();
79+
cityModel.setName("The Jungle");
80+
cityModel.setExoticProperty("lions");
81+
cityModel = cityModelRepository.save(cityModel);
82+
83+
CityModel reloaded = cityModelRepository.findById(cityModel.getCityId())
84+
.orElseThrow(RuntimeException::new);
85+
assertThat(reloaded.getExoticProperty()).isEqualTo("lions");
86+
87+
long cnt = cityModelRepository.deleteAllByExoticProperty("lions");
88+
assertThat(cnt).isOne();
89+
}
90+
91+
@Test
92+
public void testSortOnExoticProperty() {
93+
94+
Sort sort = Sort.by(Sort.Order.asc("exoticProperty"));
95+
List<CityModel> cityModels = cityModelRepository.findAll(sort);
96+
97+
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
98+
}
99+
100+
@Test
101+
public void testSortOnExoticPropertyCustomQuery_MakeSureIUnderstand() {
102+
103+
Sort sort = Sort.by(Sort.Order.asc("n.name"));
104+
List<CityModel> cityModels = cityModelRepository.customQuery(sort);
105+
106+
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Cars", "Bikes");
107+
}
108+
109+
@Test
110+
public void testSortOnExoticPropertyCustomQuery() {
111+
Sort sort = Sort.by(Sort.Order.asc("n.`exotic.property`"));
112+
List<CityModel> cityModels = cityModelRepository.customQuery(sort);
113+
114+
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
115+
}
116+
117+
@Configuration
118+
@EnableTransactionManagement
119+
@EnableNeo4jRepositories
120+
static class Config extends AbstractNeo4jConfig {
121+
122+
@Bean
123+
public BookmarkCapture bookmarkCapture() {
124+
return new BookmarkCapture();
125+
}
126+
127+
@Override
128+
public PlatformTransactionManager transactionManager(
129+
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
130+
131+
BookmarkCapture bookmarkCapture = bookmarkCapture();
132+
return new Neo4jTransactionManager(driver, databaseNameProvider,
133+
Neo4jBookmarkManager.create(bookmarkCapture));
134+
}
135+
136+
@Bean
137+
public Driver driver() {
138+
139+
return neo4jConnectionSupport.getDriver();
140+
}
141+
}
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.gh2474;
17+
18+
import lombok.Data;
19+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
20+
import org.springframework.data.neo4j.core.schema.Id;
21+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
22+
import org.springframework.data.neo4j.core.schema.TargetNode;
23+
24+
/**
25+
* @author Stephen Jackson
26+
*/
27+
@RelationshipProperties
28+
@Data
29+
public class JobRelationship {
30+
@Id
31+
@GeneratedValue
32+
private Long id;
33+
34+
@TargetNode
35+
private PersonModel person;
36+
37+
private String jobTitle;
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.gh2474;
17+
18+
import lombok.Data;
19+
20+
import java.util.UUID;
21+
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 Stephen Jackson
28+
*/
29+
@Node
30+
@Data
31+
public class PersonModel {
32+
@Id
33+
@GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class)
34+
private UUID personId;
35+
36+
private String address;
37+
private String name;
38+
private String favoriteFood;
39+
}

0 commit comments

Comments
 (0)