diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index 36be9ae42..4125dc163 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -490,13 +490,18 @@ public enum CypherGenerator { if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) { expression = Cypher.raw(property); } else if (property.contains(".")) { - String[] path = property.split("\\."); - if (path.length != 2) { - throw new IllegalArgumentException(String.format( - "Cannot handle order property `%s`, it must be a simple property or one-hop path.", - property)); + int firstDot = property.indexOf('.'); + String tail = property.substring(firstDot + 1); + if (tail.isEmpty() || property.lastIndexOf(".") != firstDot) { + if (tail.trim().matches("`.+`")) { + tail = tail.replaceFirst("`(.+)`", "$1"); + } else { + throw new IllegalArgumentException(String.format( + "Cannot handle order property `%s`, it must be a simple property or one-hop path.", + property)); + } } - expression = Cypher.property(path[0], path[1]); + expression = Cypher.property(property.substring(0, firstDot), tail); } else { expression = Cypher.name(property); } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java index 3c9063bf7..68d8655e0 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java @@ -176,6 +176,13 @@ class CypherGeneratorTest { .withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path\\."); } + @Test // GH-2474 + void shouldNotFailOnMultipleEscapedHops() { + + Optional fragment = Optional.ofNullable(CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n.`a.b.c`"))); + assertThat(fragment).hasValue("ORDER BY n.`a.b.c` ASC"); + } + @CsvSource(delimiterString = "|", value = { "apoc.text.clean(department.name) |false| ORDER BY apoc.text.clean(department.name) ASC", "apoc.text.clean(department.name) |true | ORDER BY apoc.text.clean(department.name) DESC", diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModel.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModel.java new file mode 100644 index 000000000..66ef5bb28 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModel.java @@ -0,0 +1,53 @@ +/* + * Copyright 2011-2022 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.issues.gh2474; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Stephen Jackson + */ +@Node +@Data +public class CityModel { + @Id + @GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class) + private UUID cityId; + + @Relationship(value = "MAYOR") + private PersonModel mayor; + + @Relationship(value = "CITIZEN") + private List citizens = new ArrayList<>(); + + @Relationship(value = "EMPLOYEE") + private List cityEmployees = new ArrayList<>(); + + private String name; + + @Property("exotic.property") + private String exoticProperty; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java new file mode 100644 index 000000000..b5e03c543 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011-2022 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.issues.gh2474; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +/** + * @author Stephen Jackson + * @author Michael J. Simons + */ +public interface CityModelRepository extends Neo4jRepository { + + @Query("" + + "MATCH (n:CityModel)" + + "RETURN n :#{orderBy(#sort)}") + List customQuery(Sort sort); + + long deleteAllByExoticProperty(String property); +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java new file mode 100644 index 000000000..347b04833 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java @@ -0,0 +1,142 @@ +/* + * Copyright 2011-2022 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.issues.gh2474; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +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.domain.Sort; +import org.springframework.data.neo4j.config.AbstractNeo4jConfig; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +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.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Stephen Jackson + * @author Michael J. Simons + */ +@Neo4jIntegrationTest +public class GH2474IT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + @Autowired + Driver driver; + + @Autowired + BookmarkCapture bookmarkCapture; + + @Autowired + CityModelRepository cityModelRepository; + + @BeforeEach + void setupData() { + + cityModelRepository.deleteAll(); + + CityModel aachen = new CityModel(); + aachen.setName("Aachen"); + aachen.setExoticProperty("Cars"); + + CityModel utrecht = new CityModel(); + utrecht.setName("Utrecht"); + utrecht.setExoticProperty("Bikes"); + + cityModelRepository.saveAll(Arrays.asList(aachen, utrecht)); + } + + @Test + public void testStoreExoticProperty() { + + CityModel cityModel = new CityModel(); + cityModel.setName("The Jungle"); + cityModel.setExoticProperty("lions"); + cityModel = cityModelRepository.save(cityModel); + + CityModel reloaded = cityModelRepository.findById(cityModel.getCityId()) + .orElseThrow(RuntimeException::new); + assertThat(reloaded.getExoticProperty()).isEqualTo("lions"); + + long cnt = cityModelRepository.deleteAllByExoticProperty("lions"); + assertThat(cnt).isOne(); + } + + @Test + public void testSortOnExoticProperty() { + + Sort sort = Sort.by(Sort.Order.asc("exoticProperty")); + List cityModels = cityModelRepository.findAll(sort); + + assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars"); + } + + @Test + public void testSortOnExoticPropertyCustomQuery_MakeSureIUnderstand() { + + Sort sort = Sort.by(Sort.Order.asc("n.name")); + List cityModels = cityModelRepository.customQuery(sort); + + assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Cars", "Bikes"); + } + + @Test + public void testSortOnExoticPropertyCustomQuery() { + Sort sort = Sort.by(Sort.Order.asc("n.`exotic.property`")); + List cityModels = cityModelRepository.customQuery(sort); + + assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars"); + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories + static class Config extends AbstractNeo4jConfig { + + @Bean + public BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public PlatformTransactionManager transactionManager( + Driver driver, DatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); + } + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/JobRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/JobRelationship.java new file mode 100644 index 000000000..b30774e48 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/JobRelationship.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011-2022 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.issues.gh2474; + +import lombok.Data; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author Stephen Jackson + */ +@RelationshipProperties +@Data +public class JobRelationship { + @Id + @GeneratedValue + private Long id; + + @TargetNode + private PersonModel person; + + private String jobTitle; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModel.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModel.java new file mode 100644 index 000000000..233790915 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModel.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011-2022 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.issues.gh2474; + +import lombok.Data; + +import java.util.UUID; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * @author Stephen Jackson + */ +@Node +@Data +public class PersonModel { + @Id + @GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class) + private UUID personId; + + private String address; + private String name; + private String favoriteFood; +}