diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 35962a5ae..000000000 --- a/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -root=true - -[*.java] -indent_style = tab -indent_size = 4 -continuation_indent_size = 8 diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index e0857eaa2..4d8405d33 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,4 +1,21 @@ + io.spring.develocity.conventions diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 740e8bd0b..c3c54d3fa 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -1,3 +1,97 @@ -= Spring Data contribution guidelines += Contributing + +== Spring Data contribution guidelines You find the contribution guidelines for Spring Data projects https://github.com/spring-projects/spring-data-build/blob/main/CONTRIBUTING.adoc[here]. + +== Building + +JDK 17, Maven and Docker are required to build Spring Data Neo4j. +A full build will be started with: + +[source,bash] +---- +./mvnw verify +---- + +SDN uses https://jspecify.dev[JSpecify] annotations and the build can optionally run https://github.com/uber/NullAway[NullAway] in a dedicated profile that can be enabled like this: + +[source,bash] +---- +./mvnw verify -Pnullaway +---- + +The above builds will use the Develocity build-caches. You can disable them as follows: + +[source,bash] +---- +./mvnw verify \ + -Ddevelocity.cache.local.enabled=false \ + -Ddevelocity.cache.remote.enabled=false +---- + +The integration tests are able to use a locally running Neo4j instance, too: + +[source,bash] +---- +SDN_NEO4J_URL=bolt://localhost:7687 \ +SDN_NEO4J_PASSWORD=verysecret \ +./mvnw verify +---- + +There's a `fast` profile that will skip all the tests and validations: + +[source,bash] +---- +./mvnw package -Pfast +---- + +Build the documentation as follows: + +[source,bash] +---- +./mvnw process-resources -Pantora-process-resources +./mvnw antora:antora -Pfast,antora +---- + + +== Tasks + +=== Keep the build descriptor (`pom.xml`) sorted + +[source,bash] +---- +./mvnw sortpom:sort +---- + +=== Formatting sources / adding headers + +When you add new files, you can run + +[source,bash] +---- +./mvnw license:format +---- + +to add required headers automatically. + +We use https://github.com/spring-io/spring-javaformat[spring-javaformat] to format the source files. + +[source,bash] +---- +./mvnw spring-javaformat:apply +---- + +TIP: The Spring Developers write: "The source formatter does not fundamentally change your code. For example, it will not change the order of import statements. It is effectively limited to adding or removing whitespace and line feeds." + This means the following checkstyle check might still fail. + Some common errors: + + + Static imports, import `javax.*` and `java.*` before others + + + Static imports are helpful, yes, but when working with 2 builders in the same project (here jOOQ and Cypher-DSL), they can be quite confusing. + +There are plugins for https://github.com/spring-io/spring-javaformat#eclipse[Eclipse] and https://github.com/spring-io/spring-javaformat#intellij-idea[IntelliJ IDEA] and the Checkstyle settings https://github.com/spring-io/spring-javaformat#checkstyle-idea-plugin[can be imported as well]. +We took those "as is" and just disabled the lambda check (requiring even single parameters to have parenthesis). + +Public classes do require an author tag. +Please add yourself as an `@author` to the `.java` files you added or that modified substantially (more than cosmetic changes). \ No newline at end of file diff --git a/README.adoc b/README.adoc index 306c5af1e..de574c1d1 100644 --- a/README.adoc +++ b/README.adoc @@ -139,7 +139,7 @@ class MyService { Person michael = new Person("Michael"); // Persist entities and relationships to graph database - return repository.saveAll(Flux.just(emil, gerrit, michael)); + return this.repository.saveAll(Flux.just(emil, gerrit, michael)); } } ---- diff --git a/ci/clean.sh b/ci/clean.sh index 3d511353a..12e82f469 100755 --- a/ci/clean.sh +++ b/ci/clean.sh @@ -1,4 +1,20 @@ #!/bin/bash -x +# +# Copyright 2011-2025 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. +# + set -euo pipefail diff --git a/ci/pipeline.properties b/ci/pipeline.properties index a79feac95..629d54718 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,3 +1,19 @@ +# +# Copyright 2011-2025 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. +# + # Java versions java.main.tag=24.0.1_9-jdk-noble java.next.tag=24.0.1_9-jdk-noble diff --git a/ci/test.sh b/ci/test.sh index ea820843d..30ed8c6cb 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,4 +1,20 @@ #!/bin/bash -x +# +# Copyright 2011-2025 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. +# + set -euo pipefail diff --git a/etc/checkstyle/config.xml b/etc/checkstyle/config.xml index be6280c58..29f7de079 100644 --- a/etc/checkstyle/config.xml +++ b/etc/checkstyle/config.xml @@ -1,144 +1,66 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + + + + + diff --git a/etc/checkstyle/java-header.txt b/etc/checkstyle/java-header.txt deleted file mode 100644 index 7be21cfed..000000000 --- a/etc/checkstyle/java-header.txt +++ /dev/null @@ -1,17 +0,0 @@ -^\Q/*\E$ -^\Q * Copyright 2011-20\E\d\d\Q the original author or authors.\E$ -^\Q *\E$ -^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$ -^\Q * you may not use this file except in compliance with the License.\E$ -^\Q * You may obtain a copy of the License at\E$ -^\Q *\E$ -^\Q * https://www.apache.org/licenses/LICENSE-2.0\E$ -^\Q *\E$ -^\Q * Unless required by applicable law or agreed to in writing, software\E$ -^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ -^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$ -^\Q * See the License for the specific language governing permissions and\E$ -^\Q * limitations under the License.\E$ -^\Q */\E$ -^\Qpackage\E .+;$ -^.*$ diff --git a/etc/checkstyle/suppressions.xml b/etc/checkstyle/suppressions.xml index d244e919b..3384761d7 100644 --- a/etc/checkstyle/suppressions.xml +++ b/etc/checkstyle/suppressions.xml @@ -1,7 +1,23 @@ + + "-//Puppy Crawl//DTD Suppressions 1.1//EN" + "https://www.puppycrawl.com/dtds/suppressions_1_1.dtd"> - diff --git a/etc/license.tpl b/etc/license.tpl new file mode 100644 index 000000000..2b5c24b4f --- /dev/null +++ b/etc/license.tpl @@ -0,0 +1,13 @@ +Copyright 2011-${year} 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. \ No newline at end of file diff --git a/etc/migrate-to-element-id.yml b/etc/migrate-to-element-id.yml index 7d92261ff..fbca9e750 100644 --- a/etc/migrate-to-element-id.yml +++ b/etc/migrate-to-element-id.yml @@ -1,3 +1,19 @@ +# +# Copyright 2011-2025 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. +# + # Run with # ./mvnw org.openrewrite.maven:rewrite-maven-plugin:dryRun \ diff --git a/pom.xml b/pom.xml index 07b91bff0..cdaeb55df 100644 --- a/pom.xml +++ b/pom.xml @@ -1,19 +1,22 @@ - + + + Copyright 2011-2025 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. + +--> + 4.0.0 @@ -73,7 +76,7 @@ 0.23.1 1.0.8.RELEASE ${skipTests} - 8.40 + 10.20.1 2024.5.1 spring-data-neo4j SDNEO4J @@ -89,6 +92,8 @@ 3.0.2 2021.0.1 2.2.0 + 5.0.0 + 3.6.0 3.1.4 3.7.1 ${java.version} @@ -103,14 +108,35 @@ 1.2.1 true ${skipTests} - ${skipTests} - + 4.0.0 + 0.0.46 4.0.0-SNAPSHOT + + io.r2dbc + r2dbc-bom + ${r2dbc.releasetrain} + pom + import + + + org.neo4j + neo4j-cypher-dsl-bom + ${cypher-dsl.version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers} + pom + import + com.google.code.findbugs jsr305 @@ -121,24 +147,11 @@ archunit ${archunit.version} - - eu.michael-simons.neo4j - junit-jupiter-causal-cluster-testcontainer-extension - ${junit-cc-testcontainer} - test - io.projectreactor.tools blockhound ${blockhound.version} - - io.r2dbc - r2dbc-bom - ${r2dbc.releasetrain} - pom - import - io.reactivex.rxjava2 rxjava @@ -149,12 +162,6 @@ jakarta.interceptor-api ${jakarta.interceptor-api.version} - - javax.xml.bind - jaxb-api - ${jaxb.version} - provided - net.java.dev.jna jna @@ -180,13 +187,6 @@ neo4j ${neo4j.version} - - org.neo4j - neo4j-cypher-dsl-bom - ${cypher-dsl.version} - pom - import - org.neo4j.driver neo4j-java-driver @@ -218,108 +218,36 @@ ${springdata.commons} - org.testcontainers - testcontainers-bom - ${testcontainers} - pom - import + javax.xml.bind + jaxb-api + ${jaxb.version} + provided + + + eu.michael-simons.neo4j + junit-jupiter-causal-cluster-testcontainer-extension + ${junit-cc-testcontainer} + test - - com.fasterxml.jackson.core - jackson-databind - test - - - - com.google.code.findbugs - jsr305 - test - - - com.querydsl - querydsl-core - ${querydsl} - provided - - - annotations - org.jetbrains - - - - - com.tngtech.archunit - archunit - test - - - eu.michael-simons.neo4j - junit-jupiter-causal-cluster-testcontainer-extension - test - - - eu.michael-simons.neo4j - neo4j-migrations - ${neo4j-migrations.version} - test - - - io.mockk - mockk-jvm - ${mockk} - test - io.projectreactor reactor-core true - - io.projectreactor - reactor-test - test - true - - - io.projectreactor.tools - blockhound - test - - - io.r2dbc - r2dbc-h2 - test - io.reactivex.rxjava2 rxjava true - - - jakarta.enterprise - jakarta.enterprise.cdi-api - provided - jakarta.transaction jakarta.transaction-api ${jakarta.transaction-api.version} - - org.apache.openwebbeans - openwebbeans-se - ${webbeans} - test - org.apiguardian apiguardian-api @@ -332,13 +260,13 @@ org.jetbrains.kotlin kotlin-stdlib + true - annotations org.jetbrains + annotations - true org.jetbrains.kotlin @@ -355,11 +283,6 @@ kotlinx-coroutines-reactor true - - org.junit-pioneer - junit-pioneer - test - org.neo4j neo4j-cypher-dsl @@ -392,6 +315,88 @@ org.springframework.data spring-data-commons + + com.querydsl + querydsl-core + ${querydsl} + provided + + + org.jetbrains + annotations + + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + com.fasterxml.jackson.core + jackson-databind + test + + + + com.google.code.findbugs + jsr305 + test + + + com.tngtech.archunit + archunit + test + + + eu.michael-simons.neo4j + junit-jupiter-causal-cluster-testcontainer-extension + test + + + eu.michael-simons.neo4j + neo4j-migrations + ${neo4j-migrations.version} + test + + + io.mockk + mockk-jvm + ${mockk} + test + + + io.projectreactor + reactor-test + test + true + + + io.projectreactor.tools + blockhound + test + + + io.r2dbc + r2dbc-h2 + test + + + org.apache.openwebbeans + openwebbeans-se + ${webbeans} + test + + + org.junit-pioneer + junit-pioneer + test + org.springframework.data spring-data-r2dbc @@ -404,8 +409,8 @@ test - annotations org.jetbrains + annotations @@ -421,16 +426,16 @@ - junit junit + junit - annotations org.jetbrains + annotations - lombok org.projectlombok + lombok @@ -459,25 +464,83 @@ com.github.ekryd.sortpom sortpom-maven-plugin - 2.12.0 - - - verify - - sort - - - + ${sortpom-maven-plugin.version} ${project.build.sourceEncoding} true -1 true - groupId,artifactId + scope,groupId,artifactId false false + stop + strict + true + pom.xml + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + + +
${project.basedir}/etc/license.tpl
+ + 2025 + + + ** + + + **/*.adoc + **/*.cypher + **/*.tpl + **/aot.factories + **/Jenkinsfile + **/license.txt + **/LICENSE.txt + **/notice.txt + **/org.mockito.plugins.MockMaker + **/spring.tooling + +
+
+
+
+ + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + **/module-info.java + true + etc/checkstyle/config.xml + etc/checkstyle/suppressions.xml + ${project.build.sourceEncoding} + true + true + true + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + io.spring.javaformat + spring-javaformat-checkstyle + ${spring-javaformat.version} + + + org.jacoco jacoco-maven-plugin @@ -497,43 +560,56 @@ - org.apache.maven.plugins - maven-assembly-plugin + com.github.ekryd.sortpom + sortpom-maven-plugin + + + + verify + + validate + + + + + com.mycila + license-maven-plugin + + + validate + + check + + validate + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + + + + validate + + validate + true + + org.apache.maven.plugins maven-checkstyle-plugin - verify - verify + checkstyle-validation check + validate + true - - ${project.basedir}/etc/checkstyle/config.xml - ${project.basedir}/etc/checkstyle/suppressions.xml - ${project.basedir}/etc/checkstyle/java-header.txt - ${project.build.sourceEncoding} - true - true - true - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - soundtrack - X - Soundtrack - - - org.jacoco @@ -571,10 +647,10 @@ enforce - validate enforce + validate @@ -601,6 +677,9 @@ org.apache.maven.plugins maven-failsafe-plugin + + ${skipIntegrationTests} + @@ -609,33 +688,30 @@ - - ${skipIntegrationTests} - org.codehaus.mojo flatten-maven-plugin - - - flatten - process-resources - - flatten - - - - flatten.clean - clean - - clean - - - true resolveCiFriendliesOnly + + + flatten + + flatten + + process-resources + + + flatten.clean + + clean + + clean + + org.apache.maven.plugins @@ -710,6 +786,25 @@ + + fast + + + fast + + + + true + true + true + true + true + true + true + true + true + +
diff --git a/settings.xml b/settings.xml index b3227cc11..07f937a1c 100644 --- a/settings.xml +++ b/settings.xml @@ -1,3 +1,20 @@ + Optional.ofNullable(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(User.class::cast) + .map(User::getUsername) + .map(DatabaseSelection::byName) + .orElseGet(DatabaseSelection::undecided); + } +} ---- NOTE: Be careful that you don't mix up entities retrieved from one database with another database. @@ -842,9 +856,47 @@ This is our canonical movie example with the imperative template: [[imperative-template-example]] .TemplateExampleTest.java ---- -include::example$documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative-pt1] +import java.util.Collections; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.documentation.domain.MovieEntity; +import org.springframework.data.neo4j.documentation.domain.PersonEntity; +import org.springframework.data.neo4j.documentation.domain.Roles; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@Neo4jIntegrationTest @DataNeo4jTest -include::example$documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative-pt2] +public class TemplateExampleTest { + + @Test + void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) { + + MovieEntity movie = new MovieEntity("The Love Bug", + "A movie that follows the adventures of Herbie, Herbie's driver, " + + "Jim Douglas (Dean Jones), and Jim's love interest, " + "Carole Bennett (Michele Lee)"); + + Roles roles1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi")); + Roles roles2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi")); + movie.getActorsAndRoles().add(roles1); + movie.getActorsAndRoles().add(roles2); + + MovieEntity result = neo4jTemplate.save(movie); + assertThat(result.getActorsAndRoles()).allSatisfy(relationship -> assertThat(relationship.getId()).isNotNull()); + + Optional person = neo4jTemplate.findById("Dean Jones", PersonEntity.class); + assertThat(person).map(PersonEntity::getBorn).hasValue(1931); + + assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L); + } + +} ---- And here is the reactive version, omitting the setup for brevity: @@ -853,9 +905,58 @@ And here is the reactive version, omitting the setup for brevity: [[reactive-template-example]] .ReactiveTemplateExampleTest.java ---- -include::example$documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive-pt1] +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.documentation.domain.MovieEntity; +import org.springframework.data.neo4j.documentation.domain.PersonEntity; +import org.springframework.data.neo4j.documentation.domain.Roles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +@Testcontainers @DataNeo4jTest -include::example$documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive-pt2] +class ReactiveTemplateExampleTest { + + @Container + private static Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:5"); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("org.neo4j.driver.uri", neo4jContainer::getBoltUrl); + registry.add("org.neo4j.driver.authentication.username", () -> "neo4j"); + registry.add("org.neo4j.driver.authentication.password", neo4jContainer::getAdminPassword); + } + + @Test + void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) { + + MovieEntity movie = new MovieEntity("The Love Bug", + "A movie that follows the adventures of Herbie, Herbie's driver, Jim Douglas (Dean Jones), and Jim's love interest, Carole Bennett (Michele Lee)"); + + Roles role1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi")); + Roles role2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi")); + movie.getActorsAndRoles().add(role1); + movie.getActorsAndRoles().add(role2); + + StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete(); + + StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn)) + .expectNext(1931) + .verifyComplete(); + + StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete(); + } + +} ---- Please note that both examples use `@DataNeo4jTest` from Spring Boot. @@ -1075,7 +1176,7 @@ Assume the following repository _declaration_ that basically aggregates one base [[aggregating-repository]] .A repository composed of several fragments ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=aggregating-interface] +include::example$documentation/repositories/custom_queries/MovieRepository.java[lines=18..] ---- The repository contains xref:getting-started.adoc#movie-entity[Movies] as shown in xref:getting-started.adoc#example-node-spring-boot-project[the getting started section]. @@ -1091,7 +1192,7 @@ The fragment `DomainResults` declares one additional method `findMoviesAlongShor [[domain-results]] .DomainResults fragment ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=domain-results] +include::example$documentation/repositories/custom_queries/DomainResults.java[lines=18..] ---- This method is annotated with `@Transactional(readOnly = true)` to indicate that readers can answer it. @@ -1103,7 +1204,7 @@ The implementation has the same name with the suffix `Impl`: [[domain-results-impl]] .A fragment implementation using the Neo4jTemplate ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=domain-results-impl] +include::example$documentation/repositories/custom_queries/DomainResultsImpl.java[lines=18..] ---- <.> The `Neo4jTemplate` is injected by the runtime through the constructor of `DomainResultsImpl`. No need for `@Autowired`. <.> The Cypher-DSL is used to build a complex statement (pretty much the same as shown in <>.) @@ -1137,7 +1238,7 @@ Declaring the fragment is exactly the same as before: [[non-domain-results]] .A fragment declaring non-domain-type results ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=non-domain-results] +include::example$documentation/repositories/custom_queries/NonDomainResults.java[lines=18..] ---- <.> This is a made up non-domain result. A real world query result would probably look more complex. <.> The method this fragment adds. Again, the method is annotated with Spring's `@Transactional` @@ -1148,7 +1249,7 @@ Without an implementation for that fragment, startup would fail, so here it is: [[non-domain-results-impl]] .A fragment implementation using the Neo4jClient ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=non-domain-results-impl] +include::example$documentation/repositories/custom_queries/NonDomainResultsImpl.java[lines=18..] ---- <.> Here we use the `Neo4jClient`, as provided by the infrastructure. <.> The client takes only in Strings, but the Cypher-DSL can still be used when rendering into a String @@ -1168,7 +1269,9 @@ with the Neo4j Java-Driver. This is possible as well. The following example show [[low-level-interactions]] .Fragments using the plain driver ---- -include::example$documentation/repositories/custom_queries/MovieRepository.java[tags=lowlevel-interactions] +include::example$documentation/repositories/custom_queries/LowlevelInteractions.java[lines=18..] + +include::example$documentation/repositories/custom_queries/LowlevelInteractionsImpl.java[lines=18..] ---- <.> Work with the driver directly. As with all the examples: There is no need for `@Autowired` magic. All the fragments are actually testable on their own. @@ -1234,7 +1337,24 @@ The following listing presents every configuration option provided by Spring Dat [source,java,indent=0,tabsize=4] .Enabling and configuring Neo4j auditing ---- -include::example$integration/imperative/AuditingIT.java[tags=faq.entities.auditing] +@Configuration +@EnableNeo4jAuditing(modifyOnCreate = false, // <.> + auditorAwareRef = "auditorProvider", // <.> + dateTimeProviderRef = "fixedDateTimeProvider" // <.> +) +class AuditingConfig { + + @Bean + AuditorAware auditorProvider() { + return () -> Optional.of("A user"); + } + + @Bean + DateTimeProvider fixedDateTimeProvider() { + return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE); + } + +} ---- <.> Set to true if you want the modification data to be written during creating as well <.> Use this attribute to specify the name of the bean that provides the auditor (i.e. a user name) @@ -1254,7 +1374,35 @@ The following example adds one callback to the context that changes one attribut [source,java,indent=0,tabsize=4] .Modifying entities before save ---- -include::example$integration/imperative/CallbacksIT.java[tags=faq.entities.auditing.callbacks] +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback; +import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback; +import org.springframework.data.neo4j.integration.shared.common.ThingWithAssignedId; + +@Configuration +class CallbacksConfig { + + @Bean + BeforeBindCallback nameChanger() { + return entity -> { + ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId(), + entity.getName() + " (Edited)"); + return updatedThing; + }; + } + + @Bean + AfterConvertCallback randomValueAssigner() { + return (entity, definition, source) -> { + entity.setRandomValue(UUID.randomUUID().toString()); + return entity; + }; + } + +} ---- No additional configuration is required. diff --git a/src/main/antora/modules/ROOT/pages/introduction-and-preface/package-info.java b/src/main/antora/modules/ROOT/pages/introduction-and-preface/package-info.java index 8aa6d1f9d..f81501aa2 100644 --- a/src/main/antora/modules/ROOT/pages/introduction-and-preface/package-info.java +++ b/src/main/antora/modules/ROOT/pages/introduction-and-preface/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * This package contains configuration related support classes that can be used for application specific, annotated diff --git a/src/main/antora/modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc b/src/main/antora/modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc index fa8599dc4..ceb5c6483 100644 --- a/src/main/antora/modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc @@ -81,7 +81,59 @@ We also support interfaces in domain-class-hierarchies for some scenarios: .Domain model in a separate module, same primary label like the interface name [source,java,indent=0,tabsize=4] ---- -include::example$integration/shared/common/Inheritance.java[tag=interface1] +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +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.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipId; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +public interface SomeInterface { // <.> + + String getName(); + + SomeInterface getRelated(); +} + +@Node("SomeInterface") // <.> +public static class SomeInterfaceEntity implements SomeInterface { + + @Id + @GeneratedValue + private Long id; + + private final String name; + + private SomeInterface related; + + public SomeInterfaceEntity(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public SomeInterface getRelated() { + return related; + } + + public Long getId() { + return id; + } + + public void setRelated(SomeInterface related) { + this.related = related; + } +} ---- <.> Just the plain interface name, as you would name your domain <.> As we need to synchronize the primary labels, we put `@Node` on the implementing class, which @@ -93,7 +145,18 @@ Using a different primary label instead of the interface name is possible, too: .Different primary label [source,java,indent=0,tabsize=4] ---- -include::example$integration/shared/common/Inheritance.java[tag=interface2] +@Node("PrimaryLabelWN") // <.> +public interface SomeInterface2 { + + String getName(); + + SomeInterface2 getRelated(); +} + +public static class SomeInterfaceEntity2 implements SomeInterface { + + // Overrides omitted for brevity +} ---- <.> Put the `@Node` annotation on the interface @@ -103,7 +166,37 @@ When doing so, at least two labels are required: A label determining the interfa .Multiple implementations [source,java,indent=0,tabsize=4] ---- -include::example$integration/shared/common/Inheritance.java[tag=interface3] +@Node("SomeInterface3") // <.> +public interface SomeInterface3 { + + String getName(); + + SomeInterface3 getRelated(); +} + +@Node("SomeInterface3a") // <.> +public static class SomeInterfaceImpl3a implements SomeInterface3 { + + // Overrides omitted for brevity +} + +@Node("SomeInterface3b") // <.> +public static class SomeInterfaceImpl3b implements SomeInterface3 { + + // Overrides omitted for brevity +} + +@Node +public static class ParentModel { // <.> + + @Id + @GeneratedValue + private Long id; + + private SomeInterface3 related1; // <.> + + private SomeInterface3 related2; +} ---- <.> Explicitly specifying the label that identifies the interface is required in this scenario <.> Which applies for the first… @@ -111,12 +204,43 @@ include::example$integration/shared/common/Inheritance.java[tag=interface3] <.> This is a client or parent model, using `SomeInterface3` transparently for two relationships <.> No concrete type is specified -The data structure needed is shown in the following test. The same would be written by the OGM: +The data structure needed is shown in the following test: .Data structure needed for using multiple, different interface implementations [source,java,indent=0,tabsize=4] ---- -include::example$integration/imperative/InheritanceMappingIT.java[tag=interface3] +void mixedImplementationsRead(@Autowired Neo4jTemplate template) { + + Long id; + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction + .run(""" + CREATE (s:ParentModel{name:'s'}) + CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) + CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) + RETURN id(s)""") + .single() + .get(0) + .asLong(); + transaction.commit(); + } + + Optional optionalParentModel = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.ParentModel.class)); + + assertThat(optionalParentModel).hasValueSatisfying(v -> { + assertThat(v.getName()).isEqualTo("s"); + assertThat(v).extracting(Inheritance.ParentModel::getRelated1) + .isInstanceOf(Inheritance.SomeInterfaceImpl3b.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("3b"); + assertThat(v).extracting(Inheritance.ParentModel::getRelated2) + .isInstanceOf(Inheritance.SomeInterfaceImpl3a.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("3a"); + }); +} ---- NOTE: Interfaces cannot define an identifier field. @@ -216,7 +340,34 @@ A relationship property class and its usage may look like this: .Relationship properties `Roles` [source,java] ---- -include::example$documentation/domain/Roles.java[tags=mapping.relationship.properties] +@RelationshipProperties +public class Roles { + + @RelationshipId + private Long id; + + private final List roles; + + @TargetNode + private final PersonEntity person; + + public Roles(PersonEntity person, List roles) { + this.person = person; + this.roles = roles; + } + + + public List getRoles() { + return roles; + } + + @Override + public String toString() { + return "Roles{" + + "id=" + id + + '}' + this.hashCode(); + } +} ---- You must define a property for the generated, internal ID (`@RelationshipId`) so that SDN can determine during save which relationships @@ -226,7 +377,8 @@ If SDN does not find a field for storing the internal node id, it will fail duri .Defining relationship properties for an entity [source,java,indent=0] ---- -include::example$documentation/domain/MovieEntity.java[tags=mapping.relationship.properties] +@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) +private List actorsAndRoles = new ArrayList<>(); ---- [[mapping.annotations.relationship.remarks]] diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index c433b0d5f..694127289 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -1,3 +1,19 @@ +# +# Copyright 2011-2025 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. +# + version: ${antora-component.version} prerelease: ${antora-component.prerelease} diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jAotPredicates.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jAotPredicates.java index 0353d15f3..ef3d18767 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jAotPredicates.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jAotPredicates.java @@ -15,14 +15,21 @@ */ package org.springframework.data.neo4j.aot; -import org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes; - import java.util.function.Predicate; +import org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes; + /** + * Predicates used in the AoT (native image) support. + * * @author Gerrit Meier * @since 7.0.0 */ -public class Neo4jAotPredicates { +public final class Neo4jAotPredicates { + static final Predicate> IS_SIMPLE_TYPE = Neo4jSimpleTypes.HOLDER::isSimpleType; + + private Neo4jAotPredicates() { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypes.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypes.java index 405bd7890..3b61a298f 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypes.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypes.java @@ -15,12 +15,14 @@ */ package org.springframework.data.neo4j.aot; -import org.springframework.data.domain.ManagedTypes; - import java.util.Arrays; import java.util.function.Consumer; +import org.springframework.data.domain.ManagedTypes; + /** + * The set of types managed by Neo4j. + * * @author Gerrit Meier * @since 7.0.0 */ @@ -34,29 +36,33 @@ public final class Neo4jManagedTypes implements ManagedTypes { /** * Wraps an existing {@link ManagedTypes} object with {@link Neo4jManagedTypes}. + * @param managedTypes existing types to be wrapped + * @return new instance of {@link Neo4jManagedTypes} initialized from an existing set + * of managed types */ public static Neo4jManagedTypes from(ManagedTypes managedTypes) { return new Neo4jManagedTypes(managedTypes); } /** - * Factory method used to construct {@link Neo4jManagedTypes} from the given array of {@link Class types}. - * - * @param types array of {@link Class types} used to initialize the {@link ManagedTypes}; must not be {@literal null}. - * @return new instance of {@link Neo4jManagedTypes} initialized from {@link Class types}. + * Factory method used to construct {@link Neo4jManagedTypes} from the given array of + * {@link Class types}. + * @param types array of {@link Class types} used to initialize the + * {@link ManagedTypes}; must not be {@literal null} + * @return new instance of {@link Neo4jManagedTypes} initialized from {@link Class + * types} */ public static Neo4jManagedTypes from(Class... types) { return fromIterable(Arrays.asList(types)); } /** - * Factory method used to construct {@link Neo4jManagedTypes} from the given, required {@link Iterable} of - * {@link Class types}. - * - * @param types {@link Iterable} of {@link Class types} used to initialize the {@link ManagedTypes}; must not be - * {@literal null}. - * @return new instance of {@link Neo4jManagedTypes} initialized the given, required {@link Iterable} of {@link Class - * types}. + * Factory method used to construct {@link Neo4jManagedTypes} from the given, required + * {@link Iterable} of {@link Class types}. + * @param types {@link Iterable} of {@link Class types} used to initialize the + * {@link ManagedTypes}; must not be {@literal null}. + * @return new instance of {@link Neo4jManagedTypes} initialized the given, required + * {@link Iterable} of {@link Class types}. */ public static Neo4jManagedTypes fromIterable(Iterable> types) { return from(ManagedTypes.fromIterable(types)); @@ -64,7 +70,6 @@ public final class Neo4jManagedTypes implements ManagedTypes { /** * Factory method to return an empty {@link Neo4jManagedTypes} object. - * * @return an empty {@link Neo4jManagedTypes} object. */ public static Neo4jManagedTypes empty() { @@ -73,6 +78,7 @@ public final class Neo4jManagedTypes implements ManagedTypes { @Override public void forEach(Consumer> action) { - delegate.forEach(action); + this.delegate.forEach(action); } + } diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java index 115931e7f..df0adbba0 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jManagedTypesBeanRegistrationAotProcessor.java @@ -16,12 +16,16 @@ package org.springframework.data.neo4j.aot; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.core.ResolvableType; import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor; import org.springframework.util.ClassUtils; /** + * Registered managed types and repositories to be included in AoT (native image) + * processing. + * * @author Gerrit Meier * @since 7.0.0 */ @@ -49,4 +53,5 @@ public final class Neo4jManagedTypesBeanRegistrationAotProcessor extends Managed super.contributeType(type, generationContext); } + } diff --git a/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java b/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java index 10f03ba12..c6146fda4 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java +++ b/src/main/java/org/springframework/data/neo4j/aot/Neo4jRuntimeHints.java @@ -15,7 +15,10 @@ */ package org.springframework.data.neo4j.aot; +import java.util.Arrays; + import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -36,41 +39,54 @@ import org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepo import org.springframework.data.querydsl.QuerydslUtils; import org.springframework.data.util.ReactiveWrappers; -import java.util.Arrays; - /** + * AoT runtime hints registering various types for reflection. + * * @author Gerrit Meier * @since 7.0.0 */ -public class Neo4jRuntimeHints implements RuntimeHintsRegistrar { +public final class Neo4jRuntimeHints implements RuntimeHintsRegistrar { + + private static void registerQuerydslHints(RuntimeHints hints) { + + hints.reflection() + .registerType(QuerydslNeo4jPredicateExecutor.class, MemberCategory.INVOKE_PUBLIC_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { + hints.reflection() + .registerType(ReactiveQuerydslNeo4jPredicateExecutor.class, MemberCategory.INVOKE_PUBLIC_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + + } @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - hints.reflection().registerTypes( - Arrays.asList( - TypeReference.of(SimpleNeo4jRepository.class), - TypeReference.of(SimpleQueryByExampleExecutor.class), - TypeReference.of(CypherdslConditionExecutorImpl.class), - TypeReference.of(BeforeBindCallback.class), - TypeReference.of(AfterConvertCallback.class), - // todo "temporary" fix, should get resolved when class parameters in annotations getting discovered - TypeReference.of(UUIDStringGenerator.class), - TypeReference.of(GeneratedValue.InternalIdGenerator.class), - TypeReference.of(GeneratedValue.UUIDGenerator.class) - ), - builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_METHODS)); + hints.reflection() + .registerTypes( + Arrays.asList(TypeReference.of(SimpleNeo4jRepository.class), + TypeReference.of(SimpleQueryByExampleExecutor.class), + TypeReference.of(CypherdslConditionExecutorImpl.class), + TypeReference.of(BeforeBindCallback.class), TypeReference.of(AfterConvertCallback.class), + // todo "temporary" fix, should get resolved when class + // parameters in annotations getting discovered + TypeReference.of(UUIDStringGenerator.class), + TypeReference.of(GeneratedValue.InternalIdGenerator.class), + TypeReference.of(GeneratedValue.UUIDGenerator.class)), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS)); if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { - hints.reflection().registerTypes( - Arrays.asList( - TypeReference.of(SimpleReactiveNeo4jRepository.class), - TypeReference.of(SimpleReactiveQueryByExampleExecutor.class), - TypeReference.of(ReactiveCypherdslConditionExecutorImpl.class), - TypeReference.of(ReactiveBeforeBindCallback.class) - ), - builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); + hints.reflection() + .registerTypes( + Arrays.asList(TypeReference.of(SimpleReactiveNeo4jRepository.class), + TypeReference.of(SimpleReactiveQueryByExampleExecutor.class), + TypeReference.of(ReactiveCypherdslConditionExecutorImpl.class), + TypeReference.of(ReactiveBeforeBindCallback.class)), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS)); } if (QuerydslUtils.QUERY_DSL_PRESENT) { @@ -78,15 +94,4 @@ public class Neo4jRuntimeHints implements RuntimeHintsRegistrar { } } - private static void registerQuerydslHints(RuntimeHints hints) { - - hints.reflection().registerType(QuerydslNeo4jPredicateExecutor.class, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - - if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { - hints.reflection().registerType(ReactiveQuerydslNeo4jPredicateExecutor.class, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - - } } diff --git a/src/main/java/org/springframework/data/neo4j/aot/package-info.java b/src/main/java/org/springframework/data/neo4j/aot/package-info.java index d7bad8269..5876ed473 100644 --- a/src/main/java/org/springframework/data/neo4j/aot/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/aot/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ @NullMarked package org.springframework.data.neo4j.aot; diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java index 568cd36ec..5666256d2 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.config; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -27,6 +28,7 @@ import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.UserSelectionProvider; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -34,8 +36,8 @@ import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigura import org.springframework.transaction.PlatformTransactionManager; /** - * Base class for imperative SDN configuration using JavaConfig. This can be included in all scenarios in which Spring - * Boot is not an option. + * Base class for imperative SDN configuration using JavaConfig. This can be included in + * all scenarios in which Spring Boot is not an option. * * @author Michael J. Simons * @author Gerrit Meier @@ -51,27 +53,42 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { @Autowired private ObjectProvider bookmarkManagerProviders; + @Override + public Neo4jConversions neo4jConversions() { + return super.neo4jConversions(); + } + + @Override + public org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration() { + return super.cypherDslConfiguration(); + } + + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + return super.neo4jMappingContext(neo4JConversions); + } + /** * The driver to be used for interacting with Neo4j. - * * @return the Neo4j Java driver instance to work with. */ public abstract Driver driver(); /** - * The driver used here should be the driver resulting from {@link #driver()}, which is the default. - * - * @param driver The driver to connect with. - * @return A imperative Neo4j client. + * The driver used here should be the driver resulting from {@link #driver()}, which + * is the default. + * @param driver the driver to connect with. + * @param databaseSelectionProvider the database selection provider to use. + * @return a imperative Neo4j client. */ @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) public Neo4jClient neo4jClient(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) { return Neo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) - .withNeo4jBookmarkManager(getBootBookmarkManager()) - .build(); + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) + .withNeo4jBookmarkManager(getBootBookmarkManager()) + .build(); } private Neo4jBookmarkManager getBootBookmarkManager() { @@ -85,21 +102,21 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { } /** - * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}. - * - * @param driver The driver to synchronize against - * @param databaseSelectionProvider The configured database selection provider - * @return A platform transaction manager + * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver + * resulting from {@link #driver()}. + * @param driver the driver to synchronize against + * @param databaseSelectionProvider the configured database selection provider + * @return a platform transaction manager */ @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) - public PlatformTransactionManager transactionManager(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + @Nullable DatabaseSelectionProvider databaseSelectionProvider) { - return Neo4jTransactionManager - .with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) - .withBookmarkManager(getBootBookmarkManager()) - .build(); + return Neo4jTransactionManager.with(driver) + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) + .withBookmarkManager(getBootBookmarkManager()) + .build(); } @Bean @@ -109,13 +126,13 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { /** * Configures the database selection provider. - * - * @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on - * Neo4j 3.5 and prior. + * @return the default database name provider, defaulting to the default database on + * Neo4j 4.0 and on no default on Neo4j 3.5 and prior. */ @Bean protected DatabaseSelectionProvider databaseSelectionProvider() { return DatabaseSelectionProvider.getDefaultSelectionProvider(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java index c6f854061..406c45a50 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.config; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -26,6 +27,7 @@ import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; @@ -34,8 +36,8 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.ReactiveTransactionManager; /** - * Base class for reactive SDN configuration using JavaConfig. This can be included in all scenarios in which Spring - * Boot is not an option. + * Base class for reactive SDN configuration using JavaConfig. This can be included in all + * scenarios in which Spring Boot is not an option. * * @author Gerrit Meier * @author Michael J. Simons @@ -51,36 +53,50 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp @Autowired private ObjectProvider bookmarkManagerProviders; + @Override + public org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration() { + return super.cypherDslConfiguration(); + } + + @Override + public Neo4jConversions neo4jConversions() { + return super.neo4jConversions(); + } + + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + return super.neo4jMappingContext(neo4JConversions); + } + /** * The driver to be used for interacting with Neo4j. - * * @return the Neo4j Java driver instance to work with. */ public abstract Driver driver(); /** - * The driver used here should be the driver resulting from {@link #driver()}, which is the default. - * - * @param driver The driver to connect with. - * @return A reactive Neo4j client. + * The driver used here should be the driver resulting from {@link #driver()}, which + * is the default. + * @param driver the driver to connect with + * @param databaseSelectionProvider the configured database selection provider + * @return a reactive Neo4j client */ @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) public ReactiveNeo4jClient neo4jClient(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { return ReactiveNeo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .withNeo4jBookmarkManager(getBootBookmarkManager()) - .build(); + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .withNeo4jBookmarkManager(getBootBookmarkManager()) + .build(); } private Neo4jBookmarkManager getBootBookmarkManager() { return this.bookmarkManagerProviders.getIfAvailable(Neo4jBookmarkManager::createReactive); } - @Nullable - private ReactiveUserSelectionProvider getUserSelectionProvider() { - return this.userSelectionProviders == null ? null : this.userSelectionProviders.getIfUnique(); + @Nullable private ReactiveUserSelectionProvider getUserSelectionProvider() { + return this.userSelectionProviders.getIfUnique(); } @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) @@ -91,20 +107,21 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp } /** - * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}. - * - * @param driver The driver to synchronize against - * @return A platform transaction manager + * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver + * resulting from {@link #driver()}. + * @param driver the driver to synchronize against + * @param databaseSelectionProvider the configured database selection provider + * @return a platform transaction manager */ @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { return ReactiveNeo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .withBookmarkManager(getBootBookmarkManager()) - .build(); + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .withBookmarkManager(getBootBookmarkManager()) + .build(); } @Bean @@ -114,13 +131,13 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp /** * Configures the database name provider. - * - * @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on - * Neo4j 3.5 and prior. + * @return the default database name provider, defaulting to the default database on + * Neo4j 4.0 and on no default on Neo4j 3.5 and prior. */ @Bean protected ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider() { return ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Builtin.java b/src/main/java/org/springframework/data/neo4j/config/Builtin.java index e11a28651..b50d671b5 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Builtin.java +++ b/src/main/java/org/springframework/data/neo4j/config/Builtin.java @@ -20,26 +20,26 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import jakarta.inject.Qualifier; - import org.apiguardian.api.API; /** * An internally used CDI {@link Qualifier} to mark all beans produced by our - * {@link Neo4jCdiConfigurationSupport configuration support} as built in. - * When the {@link Neo4jCdiExtension Spring Data Neo4j CDI extension} is used, - * you can opt in to override any of the following beans by providing a {@link jakarta.enterprise.inject.Produces @Produces} method with the - * corresponding return type: + * {@link Neo4jCdiConfigurationSupport configuration support} as built in. When the + * {@link Neo4jCdiExtension Spring Data Neo4j CDI extension} is used, you can opt in to + * override any of the following beans by providing a + * {@link jakarta.enterprise.inject.Produces @Produces} method with the corresponding + * return type: *
    - *
  • {@link org.springframework.data.neo4j.core.convert.Neo4jConversions}
  • - *
  • {@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}
  • - *
  • {@link org.springframework.data.neo4j.core.Neo4jOperations}
  • + *
  • {@link org.springframework.data.neo4j.core.convert.Neo4jConversions}
  • + *
  • {@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}
  • + *
  • {@link org.springframework.data.neo4j.core.Neo4jOperations}
  • *
- * The order in which the types are presented reflects the usefulness over overriding such a bean. - * You might want to add additional conversions to the mapping or provide a bean that dynamically selects a Neo4j database. - * Running a custom bean of the template or client might prove useful if you want to add additional methods. + * The order in which the types are presented reflects the usefulness over overriding such + * a bean. You might want to add additional conversions to the mapping or provide a bean + * that dynamically selects a Neo4j database. Running a custom bean of the template or + * client might prove useful if you want to add additional methods. * * @author Michael J. Simons - * @soundtrack Buckethead - SIGIL Soundtrack * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -47,4 +47,5 @@ import org.apiguardian.api.API; @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Builtin { + } diff --git a/src/main/java/org/springframework/data/neo4j/config/EnableNeo4jAuditing.java b/src/main/java/org/springframework/data/neo4j/config/EnableNeo4jAuditing.java index 62addeaa0..3f7dfb5c0 100644 --- a/src/main/java/org/springframework/data/neo4j/config/EnableNeo4jAuditing.java +++ b/src/main/java/org/springframework/data/neo4j/config/EnableNeo4jAuditing.java @@ -31,7 +31,6 @@ import org.springframework.data.domain.AuditorAware; * * @author Michael J. Simons * @since 6.0 - * @soundtrack Iron Maiden - Killers */ @Inherited @Documented @@ -41,31 +40,33 @@ import org.springframework.data.domain.AuditorAware; public @interface EnableNeo4jAuditing { /** - * Configures the {@link AuditorAware} bean to be used to look up the current principal. - * - * @return The name of the {@link AuditorAware} bean to be used to look up the current principal. + * Configures the {@link AuditorAware} bean to be used to look up the current + * principal. + * @return The name of the {@link AuditorAware} bean to be used to look up the current + * principal. */ String auditorAwareRef() default ""; /** - * Configures whether the creation and modification dates are set. Defaults to {@literal true}. - * + * Configures whether the creation and modification dates are set. Defaults to + * {@literal true}. * @return whether to set the creation and modification dates. */ boolean setDates() default true; /** - * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. - * + * Configures whether the entity shall be marked as modified on creation. Defaults to + * {@literal true}. * @return whether to mark the entity as modified on creation. */ boolean modifyOnCreate() default true; /** - * Configures a {@link DateTimeProvider} bean name that allows customizing actual date time class to be used for - * setting creation and modification dates. - * - * @return The name of the {@link DateTimeProvider} bean to provide the current date time for creation and modification dates. + * Configures a {@link DateTimeProvider} bean name that allows customizing actual date + * time class to be used for setting creation and modification dates. + * @return The name of the {@link DateTimeProvider} bean to provide the current date + * time for creation and modification dates. */ String dateTimeProviderRef() default ""; + } diff --git a/src/main/java/org/springframework/data/neo4j/config/EnableReactiveNeo4jAuditing.java b/src/main/java/org/springframework/data/neo4j/config/EnableReactiveNeo4jAuditing.java index fd5f15917..565e3fe5c 100644 --- a/src/main/java/org/springframework/data/neo4j/config/EnableReactiveNeo4jAuditing.java +++ b/src/main/java/org/springframework/data/neo4j/config/EnableReactiveNeo4jAuditing.java @@ -27,11 +27,11 @@ import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.AuditorAware; /** - * Annotation to enable auditing for SDN entities using reactive infrastructure via annotation configuration. + * Annotation to enable auditing for SDN entities using reactive infrastructure via + * annotation configuration. * * @author Michael J. Simons * @since 6.0 - * @soundtrack Ferris MC - Missglückte Asimetrie */ @Inherited @Documented @@ -41,31 +41,33 @@ import org.springframework.data.domain.AuditorAware; public @interface EnableReactiveNeo4jAuditing { /** - * Configures the {@link AuditorAware} bean to be used to look up the current principal. - * - * @return The name of the {@link AuditorAware} bean to be used to look up the current principal. + * Configures the {@link AuditorAware} bean to be used to look up the current + * principal. + * @return The name of the {@link AuditorAware} bean to be used to look up the current + * principal. */ String auditorAwareRef() default ""; /** - * Configures whether the creation and modification dates are set. Defaults to {@literal true}. - * + * Configures whether the creation and modification dates are set. Defaults to + * {@literal true}. * @return whether to set the creation and modification dates. */ boolean setDates() default true; /** - * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. - * + * Configures whether the entity shall be marked as modified on creation. Defaults to + * {@literal true}. * @return whether to mark the entity as modified on creation. */ boolean modifyOnCreate() default true; /** - * Configures a {@link DateTimeProvider} bean name that allows customizing actual date time class to be used for - * setting creation and modification dates. - * - * @return The name of the {@link DateTimeProvider} bean to provide the current date time for creation and modification dates. + * Configures a {@link DateTimeProvider} bean name that allows customizing actual date + * time class to be used for setting creation and modification dates. + * @return The name of the {@link DateTimeProvider} bean to provide the current date + * time for creation and modification dates. */ String dateTimeProviderRef() default ""; + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrar.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrar.java index 3cd36b8b7..c69a4a74c 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrar.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.config; +import java.lang.annotation.Annotation; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -25,59 +27,42 @@ import org.springframework.data.config.ParsingUtils; import org.springframework.data.neo4j.core.mapping.callback.AuditingBeforeBindCallback; import org.springframework.util.Assert; -import java.lang.annotation.Annotation; - /** + * Registers all beans required for the auditing support. + * * @author Michael J. Simons - * @soundtrack Iron Maiden - Killers * @since 6.0 */ final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { private static final String AUDITING_HANDLER_BEAN_NAME = "neo4jAuditingHandler"; - private static final String MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext"; - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() - */ @Override protected Class getAnnotation() { return EnableNeo4jAuditing.class; } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() - */ @Override protected String getAuditingHandlerBeanName() { return AUDITING_HANDLER_BEAN_NAME; } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry) - */ @Override protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, - BeanDefinitionRegistry registry) { + BeanDefinitionRegistry registry) { Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null"); Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder - .rootBeanDefinition(AuditingBeforeBindCallback.class); - listenerBeanDefinitionBuilder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + .rootBeanDefinition(AuditingBeforeBindCallback.class); + listenerBeanDefinitionBuilder.addConstructorArgValue( + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), AuditingBeforeBindCallback.class.getName(), registry); } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) - */ @Override protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { @@ -89,7 +74,9 @@ final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSuppor } @Override - public void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, BeanDefinitionRegistry registry) { + public void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, + BeanDefinitionRegistry registry) { builder.setFactoryMethod("from").addConstructorArgReference("neo4jMappingContext"); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java index 421c3ee22..d0eb6a488 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java @@ -15,11 +15,17 @@ */ package org.springframework.data.neo4j.config; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Driver; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jOperations; @@ -29,22 +35,16 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.transaction.PlatformTransactionManager; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Any; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Singleton; - /** - * Support class that can be used as is for all necessary CDI beans or as a blueprint for custom producers. + * Support class that can be used as is for all necessary CDI beans or as a blueprint for + * custom producers. * * @author Michael J. Simons - * @soundtrack Buckethead - SIGIL Soundtrack * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @ApplicationScoped -class Neo4jCdiConfigurationSupport { +public class Neo4jCdiConfigurationSupport { private T resolve(Instance instance) { if (!instance.isAmbiguous()) { @@ -55,50 +55,64 @@ class Neo4jCdiConfigurationSupport { return defaultInstance.get(); } - @Produces @Builtin @Singleton + @Produces + @Builtin + @Singleton public Neo4jConversions neo4jConversions() { return new Neo4jConversions(); } - @Produces @Builtin @Singleton + @Produces + @Builtin + @Singleton public DatabaseSelectionProvider databaseSelectionProvider() { return DatabaseSelectionProvider.getDefaultSelectionProvider(); } - @Produces @Builtin @Singleton + @Produces + @Builtin + @Singleton public Configuration cypherDslConfiguration() { return Configuration.defaultConfig(); } - @Produces @Builtin @Singleton - public Neo4jOperations neo4jOperations( - @Any Instance neo4jClient, - @Any Instance mappingContext, - @Any Instance cypherDslConfiguration, - @Any Instance transactionManager - ) { + @Produces + @Builtin + @Singleton + public Neo4jOperations neo4jOperations(@Any Instance neo4jClient, + @Any Instance mappingContext, @Any Instance cypherDslConfiguration, + @Any Instance transactionManager) { Neo4jTemplate neo4jTemplate = new Neo4jTemplate(resolve(neo4jClient), resolve(mappingContext)); neo4jTemplate.setCypherRenderer(Renderer.getRenderer(resolve(cypherDslConfiguration))); neo4jTemplate.setTransactionManager(resolve(transactionManager)); return neo4jTemplate; } - @Produces @Singleton + @Produces + @Singleton public Neo4jClient neo4jClient(@SuppressWarnings("CdiInjectionPointsInspection") Driver driver) { return Neo4jClient.create(driver); } - @Produces @Singleton - public Neo4jMappingContext neo4jMappingContext(@SuppressWarnings("CdiInjectionPointsInspection") Driver driver, @Any Instance neo4JConversions) { + @Produces + @Singleton + public Neo4jMappingContext neo4jMappingContext(@SuppressWarnings("CdiInjectionPointsInspection") Driver driver, + @Any Instance neo4JConversions) { - return Neo4jMappingContext.builder().withNeo4jConversions(resolve(neo4JConversions)).withTypeSystem(TypeSystem.getDefault()).build(); + return Neo4jMappingContext.builder() + .withNeo4jConversions(resolve(neo4JConversions)) + .withTypeSystem(TypeSystem.getDefault()) + .build(); } - @Produces @Singleton + @Produces + @Singleton public PlatformTransactionManager transactionManager( - @SuppressWarnings("CdiInjectionPointsInspection") Driver driver, @Any Instance databaseNameProvider) { + @SuppressWarnings("CdiInjectionPointsInspection") Driver driver, + @Any Instance databaseNameProvider) { return new Neo4jTransactionManager(driver, resolve(databaseNameProvider)); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java index 2b5c7af46..6e8750cdc 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java @@ -26,26 +26,28 @@ import jakarta.enterprise.inject.spi.AfterBeanDiscovery; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; import jakarta.enterprise.util.AnnotationLiteral; - import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; + import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryCdiBean; import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; /** - * This CDI extension enables Spring Data Neo4j on a CDI 2.0 compatible CDI container. It creates a Neo4j client, template - * and brings in the Neo4j repository mechanism as well. It is the main entry point to our CDI support. + * This CDI extension enables Spring Data Neo4j on a CDI 2.0 compatible CDI container. It + * creates a Neo4j client, template and brings in the Neo4j repository mechanism as well. + * It is the main entry point to our CDI support. *

- * It requires the presence of a Neo4j Driver bean. Other beans, like the {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} - * can be overwritten by providing a producer of it. If such a producer or bean is added, it must not use any {@link jakarta.inject.Qualifier @Qualifier} - * on the bean. + * It requires the presence of a Neo4j Driver bean. Other beans, like the + * {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} can be overwritten + * by providing a producer of it. If such a producer or bean is added, it must not use any + * {@link jakarta.inject.Qualifier @Qualifier} on the bean. *

- * This CDI extension can be used either via a build in service loader mechanism or through building a context manually. + * This CDI extension can be used either via a build in service loader mechanism or + * through building a context manually. * * @author Michael J. Simons - * @soundtrack Juse Ju - Millennium * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -55,15 +57,18 @@ public final class Neo4jCdiExtension extends CdiRepositoryExtensionSupport { * An annotation literal used for selecting default CDI beans. */ public static final AnnotationLiteral DEFAULT_BEAN = new AnnotationLiteral() { - @Override public Class annotationType() { + @Override + public Class annotationType() { return Default.class; } }; + /** * An annotation literal used for selecting {@link Any @Any} annotated beans. */ public static final AnnotationLiteral ANY_BEAN = new AnnotationLiteral() { - @Override public Class annotationType() { + @Override + public Class annotationType() { return Any.class; } }; @@ -87,15 +92,12 @@ public final class Neo4jCdiExtension extends CdiRepositoryExtensionSupport { Class repositoryType = entry.getKey(); Set qualifiers = entry.getValue(); - Neo4jRepositoryFactoryCdiBean repositoryBean = new Neo4jRepositoryFactoryCdiBean<>( - qualifiers, - repositoryType, - beanManager, - optionalCustomRepositoryImplementationDetector - ); + Neo4jRepositoryFactoryCdiBean repositoryBean = new Neo4jRepositoryFactoryCdiBean<>(qualifiers, + repositoryType, beanManager, optionalCustomRepositoryImplementationDetector); registerBean(repositoryBean); event.addBean(repositoryBean); } } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java index b017fe8cb..7843aa0c6 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java @@ -16,20 +16,23 @@ package org.springframework.data.neo4j.config; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.renderer.Configuration; + import org.springframework.context.annotation.Bean; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Node; /** - * Internal support class for basic configuration. The support infrastructure here is basically all around finding out - * about which classes are to be mapped and which not. The driver needs to be configured from a class either extending - * {@link AbstractNeo4jConfig} for imperative or {@link AbstractReactiveNeo4jConfig} for reactive programming model. + * Internal support class for basic configuration. The support infrastructure here is + * basically all around finding out about which classes are to be mapped and which not. + * The driver needs to be configured from a class either extending + * {@link AbstractNeo4jConfig} for imperative or {@link AbstractReactiveNeo4jConfig} for + * reactive programming model. * * @author Michael J. Simons * @author Gerrit Meier @@ -39,23 +42,25 @@ import org.springframework.data.neo4j.core.schema.Node; abstract class Neo4jConfigurationSupport { @Bean - public Neo4jConversions neo4jConversions() { + Neo4jConversions neo4jConversions() { return new Neo4jConversions(); } @Bean - public Configuration cypherDslConfiguration() { + Configuration cypherDslConfiguration() { return Configuration.defaultConfig(); } /** - * Creates a {@link Neo4jMappingContext} equipped with entity classes scanned from the mapping base package. - * - * @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set. + * Creates a {@link Neo4jMappingContext} equipped with entity classes scanned from the + * mapping base package. + * @param neo4JConversions the conversion system to use + * @return a new {@link Neo4jMappingContext} with initial classes to scan for entities + * set. * @see #getMappingBasePackages() */ @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -64,30 +69,32 @@ abstract class Neo4jConfigurationSupport { } /** - * Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the - * configuration class' (the concrete class, not this one here) by default. So if you have a - * {@code com.acme.AppConfig} extending {@link Neo4jConfigurationSupport} the base package will be considered + * Returns the base packages to scan for Neo4j mapped entities at startup. Will return + * the package name of the configuration class' (the concrete class, not this one + * here) by default. So if you have a {@code com.acme.AppConfig} extending + * {@link Neo4jConfigurationSupport} the base package will be considered * {@code com.acme} unless the method is overridden to implement alternate behavior. - * - * @return the base packages to scan for mapped {@link Node} classes or an empty collection to not enable scanning for - * entities. + * @return the base packages to scan for mapped {@link Node} classes or an empty + * collection to not enable scanning for entities. */ protected Collection getMappingBasePackages() { Package mappingBasePackage = getClass().getPackage(); - return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName()); + return (mappingBasePackage != null) ? List.of(mappingBasePackage.getName()) : List.of(); } /** - * Scans the mapping base package for classes annotated with {@link Node}. By default, it scans for entities in all - * packages returned by {@link #getMappingBasePackages()}. - * + * Scans the mapping base package for classes annotated with {@link Node}. By default, + * it scans for entities in all packages returned by + * {@link #getMappingBasePackages()}. * @return initial set of domain classes - * @throws ClassNotFoundException if the given class cannot be found in the class path. + * @throws ClassNotFoundException if the given class cannot be found in the class + * path. * @see #getMappingBasePackages() */ protected final Set> getInitialEntitySet() throws ClassNotFoundException { return Neo4jEntityScanner.get().scan(getMappingBasePackages()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java index 03a13e86a..fc2d805f6 100644 --- a/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jEntityScanner.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -36,15 +37,27 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** - * A utility class providing a way to discover an initial entity set for a {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext}. + * A utility class providing a way to discover an initial entity set for a + * {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext}. * * @author Michael J. Simons - * @soundtrack Kelis - Tasty * @since 6.0.2 */ @API(status = API.Status.STABLE, since = "6.0.2") public final class Neo4jEntityScanner { + @Nullable + private final ResourceLoader resourceLoader; + + /** + * Create a new {@link Neo4jEntityScanner} instance. + * @param resourceLoader an optional resource loader used for class scanning. + */ + private Neo4jEntityScanner(@Nullable ResourceLoader resourceLoader) { + + this.resourceLoader = resourceLoader; + } + public static Neo4jEntityScanner get() { return new Neo4jEntityScanner(null); @@ -55,22 +68,30 @@ public final class Neo4jEntityScanner { return new Neo4jEntityScanner(resourceLoader); } - @Nullable - private final ResourceLoader resourceLoader; - /** - * Create a new {@link Neo4jEntityScanner} instance. - * - * @param resourceLoader an optional resource loader used for class scanning. + * Create a {@link ClassPathScanningCandidateComponentProvider} to scan entities based + * on the specified {@link ApplicationContext}. + * @param resourceLoader an optional {@link ResourceLoader} to use + * @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan for + * Neo4j entities */ - private Neo4jEntityScanner(@Nullable ResourceLoader resourceLoader) { + private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider( + @Nullable ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; + ClassPathScanningCandidateComponentProvider delegate = new ClassPathScanningCandidateComponentProvider(false); + if (resourceLoader != null) { + delegate.setResourceLoader(resourceLoader); + } + + delegate.addIncludeFilter(new AnnotationTypeFilter(Node.class)); + delegate.addIncludeFilter(new AnnotationTypeFilter(Persistent.class)); + delegate.addIncludeFilter(new AnnotationTypeFilter(RelationshipProperties.class)); + + return delegate; } /** * Scan for entities with the specified annotations. - * * @param basePackages the list of base packages to scan. * @return a set of entity classes * @throws ClassNotFoundException if an entity class cannot be loaded @@ -81,7 +102,6 @@ public final class Neo4jEntityScanner { /** * Scan for entities with the specified annotations. - * * @param packages the list of base packages to scan. * @return a set of entity classes * @throws ClassNotFoundException if an entity class cannot be loaded @@ -93,13 +113,11 @@ public final class Neo4jEntityScanner { return Collections.emptySet(); } - ClassPathScanningCandidateComponentProvider scanner = - createClassPathScanningCandidateComponentProvider(this.resourceLoader); + ClassPathScanningCandidateComponentProvider scanner = createClassPathScanningCandidateComponentProvider( + this.resourceLoader); - ClassLoader classLoader = - this.resourceLoader == null ? - Neo4jConfigurationSupport.class.getClassLoader() : - this.resourceLoader.getClassLoader(); + ClassLoader classLoader = (this.resourceLoader != null) ? this.resourceLoader.getClassLoader() + : Neo4jConfigurationSupport.class.getClassLoader(); Set> entitySet = new HashSet<>(); for (String basePackage : packages) { @@ -115,24 +133,4 @@ public final class Neo4jEntityScanner { return entitySet; } - /** - * Create a {@link ClassPathScanningCandidateComponentProvider} to scan entities based - * on the specified {@link ApplicationContext}. - * - * @param resourceLoader an optional {@link ResourceLoader} to use - * @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan for Neo4j entities - */ - private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(@Nullable ResourceLoader resourceLoader) { - - ClassPathScanningCandidateComponentProvider delegate = new ClassPathScanningCandidateComponentProvider(false); - if (resourceLoader != null) { - delegate.setResourceLoader(resourceLoader); - } - - delegate.addIncludeFilter(new AnnotationTypeFilter(Node.class)); - delegate.addIncludeFilter(new AnnotationTypeFilter(Persistent.class)); - delegate.addIncludeFilter(new AnnotationTypeFilter(RelationshipProperties.class)); - - return delegate; - } } diff --git a/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jAuditingRegistrar.java b/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jAuditingRegistrar.java index fc72c5913..bad756d7f 100644 --- a/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jAuditingRegistrar.java @@ -28,37 +28,27 @@ import org.springframework.data.neo4j.core.mapping.callback.ReactiveAuditingBefo import org.springframework.util.Assert; /** + * Registers all beans required for the auditing support. + * * @author Michael J. Simons - * @soundtrack Ferris MC - Missglückte Asimetrie * @since 6.0 */ final class ReactiveNeo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { private static final String AUDITING_HANDLER_BEAN_NAME = "reactiveNeo4jAuditingHandler"; + private static final String MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext"; - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() - */ @Override protected Class getAnnotation() { return EnableReactiveNeo4jAuditing.class; } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() - */ @Override protected String getAuditingHandlerBeanName() { return AUDITING_HANDLER_BEAN_NAME; } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry) - */ @Override protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, BeanDefinitionRegistry registry) { @@ -66,30 +56,32 @@ final class ReactiveNeo4jAuditingRegistrar extends AuditingBeanDefinitionRegistr Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null"); Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class); + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class); - builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.addConstructorArgValue( + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource()); - registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingBeforeBindCallback.class.getName(), registry); + registerInfrastructureBeanWithId(builder.getBeanDefinition(), + ReactiveAuditingBeforeBindCallback.class.getName(), registry); } - /* - * (non-Javadoc) - * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) - */ @Override protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { Assert.notNull(configuration, "AuditingConfiguration must not be null"); - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class); + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class); return configureDefaultAuditHandlerAttributes(configuration, builder); } @Override - public void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, BeanDefinitionRegistry registry) { + public void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, + BeanDefinitionRegistry registry) { builder.setFactoryMethod("from").addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME); } + } diff --git a/src/main/java/org/springframework/data/neo4j/config/package-info.java b/src/main/java/org/springframework/data/neo4j/config/package-info.java index ee8cb7d7d..be9b93c34 100644 --- a/src/main/java/org/springframework/data/neo4j/config/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/config/package-info.java @@ -1,9 +1,24 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - This package contains configuration related support classes that can be used for application specific, annotated - configuration classes. The abstract base classes are helpful if you don't rely on Spring Boot's autoconfiguration. - The package provides some additional annotations that enable auditing. - * + * This package contains configuration related support classes that + * can be used for application specific, annotated configuration classes. The abstract + * base classes are helpful if you don't rely on Spring Boot's autoconfiguration. The + * package provides some additional annotations that enable auditing. */ @NullMarked package org.springframework.data.neo4j.config; diff --git a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java index 015f5448c..d929475c8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java +++ b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelection.java @@ -21,11 +21,10 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; /** - * A value holder indicating a database selection based on an optional name. {@literal null} indicates to let the server - * decide. + * A value holder indicating a database selection based on an optional name. + * {@literal null} indicates to let the server decide. * * @author Michael J. Simons - * @soundtrack Rage - Reign Of Fear * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -36,6 +35,10 @@ public final class DatabaseSelection { @Nullable private final String value; + private DatabaseSelection(@Nullable String value) { + this.value = value; + } + public static DatabaseSelection undecided() { return DEFAULT_DATABASE_NAME; @@ -43,22 +46,16 @@ public final class DatabaseSelection { /** * Create a new database selection by the given databaseName. - * - * @param databaseName The database name to select the database with. - * @return A database selection + * @param databaseName the database name to select the database with. + * @return a database selection */ public static DatabaseSelection byName(String databaseName) { return new DatabaseSelection(databaseName); } - private DatabaseSelection(@Nullable String value) { - this.value = value; - } - - @Nullable - public String getValue() { - return value; + @Nullable public String getValue() { + return this.value; } @Override @@ -70,11 +67,12 @@ public final class DatabaseSelection { return false; } DatabaseSelection that = (DatabaseSelection) o; - return Objects.equals(value, that.value); + return Objects.equals(this.value, that.value); } @Override public int hashCode() { - return Objects.hash(value); + return Objects.hash(this.value); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelectionProvider.java index 6616defba..01545478d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DatabaseSelectionProvider.java +++ b/src/main/java/org/springframework/data/neo4j/core/DatabaseSelectionProvider.java @@ -16,23 +16,27 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; + import org.springframework.util.Assert; /** - * A provider interface that knows in which database repositories or either the reactive or imperative template should - * work. + * A provider interface that knows in which database repositories or either the reactive + * or imperative template should work. *

- * An instance of a database name provider is only relevant when SDN is used with a Neo4j 4.0+ cluster or server. + * An instance of a database name provider is only relevant when SDN is used with a Neo4j + * 4.0+ cluster or server. *

- * To select the default database, return an empty optional. If you return a database name, it must not be empty. The - * empty optional indicates an unset database name on the client, so that the server can decide on the default to use. + * To select the default database, return an empty optional. If you return a database + * name, it must not be empty. The empty optional indicates an unset database name on the + * client, so that the server can decide on the default to use. *

- * The provider is asked before any interaction of a repository or template with the cluster or server. That means you - * can in theory return different database names for each interaction. Be aware that you might end up with no data on - * queries or data stored to wrong database if you don't pay meticulously attention to the database you interact with. + * The provider is asked before any interaction of a repository or template with the + * cluster or server. That means you can in theory return different database names for + * each interaction. Be aware that you might end up with no data on queries or data stored + * to wrong database if you don't pay meticulously attention to the database you interact + * with. * * @author Michael J. Simons - * @soundtrack N.W.A. - Straight Outta Compton * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -40,17 +44,10 @@ import org.springframework.util.Assert; public interface DatabaseSelectionProvider { /** - * @return The selected database me to interact with. Use {@link DatabaseSelection#undecided()} to indicate the - * default database. - */ - DatabaseSelection getDatabaseSelection(); - - /** - * Creates a statically configured database selection provider always selecting the database with the given name - * {@code databaseName}. - * - * @param databaseName The database name to use, must not be null nor empty. - * @return A statically configured database name provider. + * Creates a statically configured database selection provider always selecting the + * database with the given name {@code databaseName}. + * @param databaseName the database name to use, must not be null nor empty + * @return a statically configured database name provider */ static DatabaseSelectionProvider createStaticDatabaseSelectionProvider(String databaseName) { @@ -62,20 +59,18 @@ public interface DatabaseSelectionProvider { /** * A database selection provider always returning the default selection. - * - * @return A provider for the default database name. + * @return a provider for the default database name */ static DatabaseSelectionProvider getDefaultSelectionProvider() { return DefaultDatabaseSelectionProvider.INSTANCE; } -} -enum DefaultDatabaseSelectionProvider implements DatabaseSelectionProvider { - INSTANCE; + /** + * Retrieves the database selection. + * @return the selected database me to interact with. Use + * {@link DatabaseSelection#undecided()} to indicate the default database. + */ + DatabaseSelection getDatabaseSelection(); - @Override - public DatabaseSelection getDatabaseSelection() { - return DatabaseSelection.undecided(); - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultDatabaseSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/DefaultDatabaseSelectionProvider.java new file mode 100644 index 000000000..e06d69009 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultDatabaseSelectionProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2011-2025 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.core; + +enum DefaultDatabaseSelectionProvider implements DatabaseSelectionProvider { + + INSTANCE; + + @Override + public DatabaseSelection getDatabaseSelection() { + return DatabaseSelection.undecided(); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index a0362e6f5..4c11c2091 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -37,6 +37,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Value; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -54,8 +55,8 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the - * database. + * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to + * and interact with the database. * * @author Gerrit Meier * @author Michael J. Simons @@ -64,11 +65,15 @@ import org.springframework.util.StringUtils; final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private final Driver driver; + @Nullable private final DatabaseSelectionProvider databaseSelectionProvider; + @Nullable private final UserSelectionProvider userSelectionProvider; + private final ConversionService conversionService; + private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator(); // Local bookmark manager when using outside managed transactions @@ -79,23 +84,42 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { this.driver = builder.driver; this.databaseSelectionProvider = builder.databaseSelectionProvider; this.userSelectionProvider = builder.userSelectionProvider; - this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager); + this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager); this.conversionService = new DefaultConversionService(); - Optional.ofNullable(builder.neo4jConversions).orElseGet(Neo4jConversions::new).registerConvertersIn((ConverterRegistry) conversionService); + Optional.ofNullable(builder.neo4jConversions) + .orElseGet(Neo4jConversions::new) + .registerConvertersIn((ConverterRegistry) this.conversionService); + } + + /** + * Tries to convert the given {@link RuntimeException} into a + * {@link DataAccessException} but returns the original exception if the conversation + * failed. Thus allows safe re-throwing of the return value. + * @param ex the exception to translate + * @param exceptionTranslator the {@link PersistenceExceptionTranslator} to be used + * for translation + * @return any translated exception + */ + private static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex, + PersistenceExceptionTranslator exceptionTranslator) { + RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); + return (resolved != null) ? resolved : ex; } @Override public QueryRunner getQueryRunner(DatabaseSelection databaseSelection, UserSelection impersonatedUser) { - QueryRunner queryRunner = Neo4jTransactionManager.retrieveTransaction(driver, databaseSelection, impersonatedUser); - Collection lastBookmarks = bookmarkManager.resolve().getBookmarks(); + QueryRunner queryRunner = Neo4jTransactionManager.retrieveTransaction(this.driver, databaseSelection, + impersonatedUser); + Collection lastBookmarks = this.bookmarkManager.resolve().getBookmarks(); if (queryRunner == null) { - queryRunner = driver.session(Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, databaseSelection, impersonatedUser)); + queryRunner = this.driver.session( + Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, databaseSelection, impersonatedUser)); } - return new DelegatingQueryRunner(queryRunner, lastBookmarks, bookmarkManager.resolve()::updateBookmarks); + return new DelegatingQueryRunner(queryRunner, lastBookmarks, this.bookmarkManager.resolve()::updateBookmarks); } @Override @@ -104,57 +128,8 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { this.bookmarkManager.setApplicationContext(applicationContext); } - private static class DelegatingQueryRunner implements QueryRunner { - - private final QueryRunner delegate; - private final Collection usedBookmarks; - private final BiConsumer, Collection> newBookmarkConsumer; - - private DelegatingQueryRunner(QueryRunner delegate, Collection lastBookmarks, BiConsumer, Collection> newBookmarkConsumer) { - this.delegate = delegate; - this.usedBookmarks = lastBookmarks; - this.newBookmarkConsumer = newBookmarkConsumer; - } - - @Override - public void close() { - - // We're only going to close sessions we have acquired inside the client, not something that - // has been retrieved from the tx manager. - if (this.delegate instanceof Session session) { - - session.close(); - this.newBookmarkConsumer.accept(usedBookmarks, session.lastBookmarks()); - } - } - - @Override - public Result run(String s, Value value) { - return delegate.run(s, value); - } - - @Override - public Result run(String s, Map map) { - return delegate.run(s, map); - } - - @Override - public Result run(String s, Record record) { - return delegate.run(s, record); - } - - @Override - public Result run(String s) { - return delegate.run(s); - } - - @Override - public Result run(Query query) { - return delegate.run(query); - } - } - - // Below are all the implementations (methods and classes) as defined by the contracts of Neo4jClient + // Below are all the implementations (methods and classes) as defined by the contracts + // of Neo4jClient @Override public UnboundRunnableSpec query(String cypher) { @@ -172,17 +147,98 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { } @Override - @Nullable - public DatabaseSelectionProvider getDatabaseSelectionProvider() { - return databaseSelectionProvider; + @Nullable public DatabaseSelectionProvider getDatabaseSelectionProvider() { + return this.databaseSelectionProvider; + } + + private DatabaseSelection resolveTargetDatabaseName(@Nullable String parameterTargetDatabase) { + + String value = Neo4jClient.verifyDatabaseName(parameterTargetDatabase); + if (value != null) { + return DatabaseSelection.byName(value); + } + if (this.databaseSelectionProvider != null) { + return this.databaseSelectionProvider.getDatabaseSelection(); + } + return DatabaseSelectionProvider.getDefaultSelectionProvider().getDatabaseSelection(); + } + + private UserSelection resolveUser(@Nullable String userName) { + + if (StringUtils.hasText(userName)) { + return UserSelection.impersonate(userName); + } + if (this.userSelectionProvider != null) { + return this.userSelectionProvider.getUserSelection(); + } + return UserSelectionProvider.getDefaultSelectionProvider().getUserSelection(); + } + + private static final class DelegatingQueryRunner implements QueryRunner { + + private final QueryRunner delegate; + + private final Collection usedBookmarks; + + private final BiConsumer, Collection> newBookmarkConsumer; + + private DelegatingQueryRunner(QueryRunner delegate, Collection lastBookmarks, + BiConsumer, Collection> newBookmarkConsumer) { + this.delegate = delegate; + this.usedBookmarks = lastBookmarks; + this.newBookmarkConsumer = newBookmarkConsumer; + } + + @Override + public void close() { + + // We're only going to close sessions we have acquired inside the client, not + // something that + // has been retrieved from the tx manager. + if (this.delegate instanceof Session session) { + + session.close(); + this.newBookmarkConsumer.accept(this.usedBookmarks, session.lastBookmarks()); + } + } + + @Override + public Result run(String s, Value value) { + return this.delegate.run(s, value); + } + + @Override + public Result run(String s, Map map) { + return this.delegate.run(s, map); + } + + @Override + public Result run(String s, Record record) { + return this.delegate.run(s, record); + } + + @Override + public Result run(String s) { + return this.delegate.run(s); + } + + @Override + public Result run(Query query) { + return this.delegate.run(query); + } + } /** - * Basically a holder of a cypher template supplier and a set of named parameters. It's main purpose is to orchestrate - * the running of things with a bit of logging. + * Basically a holder of a cypher template supplier and a set of named parameters. + * It's main purpose is to orchestrate the running of things with a bit of logging. */ static class RunnableStatement { + private final Supplier cypherSupplier; + + private final NamedParameters parameters; + RunnableStatement(Supplier cypherSupplier) { this(cypherSupplier, new NamedParameters()); } @@ -192,60 +248,21 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { this.parameters = parameters; } - private final Supplier cypherSupplier; - - private final NamedParameters parameters; - protected final Result runWith(QueryRunner statementRunner) { - String statementTemplate = cypherSupplier.get(); + String statementTemplate = this.cypherSupplier.get(); if (cypherLog.isDebugEnabled()) { cypherLog.debug(() -> String.format("Executing:%s%s", System.lineSeparator(), statementTemplate)); - if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { - cypherLog.trace(() -> String.format("with parameters:%s%s", System.lineSeparator(), parameters)); + if (cypherLog.isTraceEnabled() && !this.parameters.isEmpty()) { + cypherLog + .trace(() -> String.format("with parameters:%s%s", System.lineSeparator(), this.parameters)); } } - return statementRunner.run(statementTemplate, parameters.get()); + return statementRunner.run(statementTemplate, this.parameters.get()); } - } - /** - * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original - * exception if the conversation failed. Thus allows safe re-throwing of the return value. - * - * @param ex the exception to translate - * @param exceptionTranslator the {@link PersistenceExceptionTranslator} to be used for translation - * @return Any translated exception - */ - private static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex, - PersistenceExceptionTranslator exceptionTranslator) { - RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); - return resolved == null ? ex : resolved; - } - - private DatabaseSelection resolveTargetDatabaseName(@Nullable String parameterTargetDatabase) { - - String value = Neo4jClient.verifyDatabaseName(parameterTargetDatabase); - if (value != null) { - return DatabaseSelection.byName(value); - } - if (databaseSelectionProvider != null) { - return databaseSelectionProvider.getDatabaseSelection(); - } - return DatabaseSelectionProvider.getDefaultSelectionProvider().getDatabaseSelection(); - } - - private UserSelection resolveUser(@Nullable String userName) { - - if (StringUtils.hasText(userName)) { - return UserSelection.impersonate(userName); - } - if (userSelectionProvider != null) { - return userSelectionProvider.getUserSelection(); - } - return UserSelectionProvider.getDefaultSelectionProvider().getUserSelection(); } class DefaultRunnableSpec implements UnboundRunnableSpec, RunnableSpecBoundToDatabaseAndUser { @@ -291,26 +308,29 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { @Override public MappingSpec fetchAs(Class targetClass) { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, runnableStatement, - new SingleValueMappingFunction<>(conversionService, targetClass)); + return new DefaultRecordFetchSpec<>(this.databaseSelection, this.userSelection, this.runnableStatement, + new SingleValueMappingFunction<>(DefaultNeo4jClient.this.conversionService, targetClass)); } @Override public RecordFetchSpec> fetch() { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, runnableStatement, (t, r) -> r.asMap()); + return new DefaultRecordFetchSpec<>(this.databaseSelection, this.userSelection, this.runnableStatement, + (t, r) -> r.asMap()); } @Override public ResultSummary run() { - try (QueryRunner statementRunner = getQueryRunner(databaseSelection, userSelection)) { - Result result = runnableStatement.runWith(statementRunner); + try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.userSelection)) { + Result result = this.runnableStatement.runWith(statementRunner); return ResultSummaries.process(result.consume()); - } catch (RuntimeException e) { - throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (RuntimeException ex) { + throw potentiallyConvertRuntimeException(ex, DefaultNeo4jClient.this.persistenceExceptionTranslator); + } + catch (Exception exception) { + throw new RuntimeException(exception); } } @@ -326,7 +346,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { @Override public RunnableSpec to(String name) { - DefaultRunnableSpec.this.runnableStatement.parameters.add(name, value); + DefaultRunnableSpec.this.runnableStatement.parameters.add(name, this.value); return DefaultRunnableSpec.this; } @@ -335,11 +355,13 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { Assert.notNull(binder, "Binder is required"); - return bindAll(binder.apply(value)); + return bindAll(binder.apply(this.value)); } + } class DefaultRunnableSpecBoundToDatabase implements RunnableSpecBoundToDatabase { + @Override public RunnableSpecBoundToDatabaseAndUser asUser(String aUser) { @@ -371,6 +393,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { public RunnableSpec bindAll(Map parameters) { return DefaultRunnableSpec.this.bindAll(parameters); } + } class DefaultRunnableSpecBoundToUser implements RunnableSpecBoundToUser { @@ -406,7 +429,9 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { public RunnableSpec bindAll(Map parameters) { return DefaultRunnableSpec.this.bindAll(parameters); } + } + } class DefaultRecordFetchSpec implements RecordFetchSpec, MappingSpec { @@ -419,10 +444,8 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { private BiFunction mappingFunction; - DefaultRecordFetchSpec(DatabaseSelection databaseSelection, - UserSelection impersonatedUser, - RunnableStatement runnableStatement, - BiFunction mappingFunction) { + DefaultRecordFetchSpec(DatabaseSelection databaseSelection, UserSelection impersonatedUser, + RunnableStatement runnableStatement, BiFunction mappingFunction) { this.databaseSelection = databaseSelection; this.impersonatedUser = impersonatedUser; @@ -442,16 +465,18 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { public Optional one() { try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) { - Result result = runnableStatement.runWith(statementRunner); - Optional optionalValue = result.hasNext() ? - Optional.ofNullable(mappingFunction.apply(TypeSystem.getDefault(), result.single())) : - Optional.empty(); + Result result = this.runnableStatement.runWith(statementRunner); + Optional optionalValue = result.hasNext() + ? Optional.ofNullable(this.mappingFunction.apply(TypeSystem.getDefault(), result.single())) + : Optional.empty(); ResultSummaries.process(result.consume()); return optionalValue; - } catch (RuntimeException e) { - throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (RuntimeException ex) { + throw potentiallyConvertRuntimeException(ex, DefaultNeo4jClient.this.persistenceExceptionTranslator); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } @@ -459,14 +484,19 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { public Optional first() { try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) { - Result result = runnableStatement.runWith(statementRunner); - Optional optionalValue = result.stream().map(partialMappingFunction(TypeSystem.getDefault())).filter(Objects::nonNull).findFirst(); + Result result = this.runnableStatement.runWith(statementRunner); + Optional optionalValue = result.stream() + .map(partialMappingFunction(TypeSystem.getDefault())) + .filter(Objects::nonNull) + .findFirst(); ResultSummaries.process(result.consume()); return optionalValue; - } catch (RuntimeException e) { - throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (RuntimeException ex) { + throw potentiallyConvertRuntimeException(ex, DefaultNeo4jClient.this.persistenceExceptionTranslator); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } @@ -474,39 +504,41 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { public Collection all() { try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) { - Result result = runnableStatement.runWith(statementRunner); + Result result = this.runnableStatement.runWith(statementRunner); Collection values = result.stream().flatMap(r -> { - if (mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 && r.get(0).hasType(TypeSystem.getDefault().LIST())) { - return r.get(0).asList(v -> ((SingleValueMappingFunction) mappingFunction).convertValue(v)).stream(); + if (this.mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 + && r.get(0).hasType(TypeSystem.getDefault().LIST())) { + return r.get(0) + .asList(v -> ((SingleValueMappingFunction) this.mappingFunction).convertValue(v)) + .stream(); } return Stream.of(partialMappingFunction(TypeSystem.getDefault()).apply(r)); }).filter(Objects::nonNull).collect(Collectors.toList()); ResultSummaries.process(result.consume()); return values; - } catch (RuntimeException e) { - throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (RuntimeException ex) { + throw potentiallyConvertRuntimeException(ex, DefaultNeo4jClient.this.persistenceExceptionTranslator); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } - /** - * @param typeSystem The actual type system - * @return The partially evaluated mapping function - */ private Function partialMappingFunction(TypeSystem typeSystem) { - return r -> mappingFunction.apply(typeSystem, r); + return r -> this.mappingFunction.apply(typeSystem, r); } + } class DefaultRunnableDelegation implements RunnableDelegation, OngoingDelegation { + private final Function> callback; + private DatabaseSelection databaseSelection; private UserSelection impersonatedUser; - private final Function> callback; - DefaultRunnableDelegation(Function> callback) { this.callback = callback; this.databaseSelection = resolveTargetDatabaseName(null); @@ -522,13 +554,17 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { @Override public Optional run() { - try (QueryRunner queryRunner = getQueryRunner(databaseSelection, this.impersonatedUser)) { - return callback.apply(queryRunner); - } catch (RuntimeException e) { - throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator); - } catch (Exception e) { - throw new RuntimeException(e); + try (QueryRunner queryRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) { + return this.callback.apply(queryRunner); + } + catch (RuntimeException ex) { + throw potentiallyConvertRuntimeException(ex, DefaultNeo4jClient.this.persistenceExceptionTranslator); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveDatabaseSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveDatabaseSelectionProvider.java new file mode 100644 index 000000000..7087b4f37 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveDatabaseSelectionProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2025 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.core; + +import reactor.core.publisher.Mono; + +/** + * The default {@link ReactiveDatabaseSelectionProvider}. + * + * @author Michael J. Simons + */ +enum DefaultReactiveDatabaseSelectionProvider implements ReactiveDatabaseSelectionProvider { + + INSTANCE; + + @Override + public Mono getDatabaseSelection() { + return Mono.just(DatabaseSelection.undecided()); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 7ea9847fb..3ffdf91cf 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -15,6 +15,15 @@ */ package org.springframework.data.neo4j.core; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Driver; @@ -27,6 +36,11 @@ import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -42,35 +56,25 @@ import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionM import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; - /** * Reactive variant of the {@link Neo4jClient}. * * @author Michael J. Simons * @author Gerrit Meier - * @soundtrack Die Toten Hosen - Im Auftrag des Herrn * @since 6.0 */ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, ApplicationContextAware { private final Driver driver; + @Nullable private final ReactiveDatabaseSelectionProvider databaseSelectionProvider; + @Nullable private final ReactiveUserSelectionProvider userSelectionProvider; + private final ConversionService conversionService; + private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator(); // Local bookmark manager when using outside managed transactions @@ -83,89 +87,51 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati this.userSelectionProvider = builder.impersonatedUserProvider; this.conversionService = new DefaultConversionService(); - Optional.ofNullable(builder.neo4jConversions).orElseGet(Neo4jConversions::new).registerConvertersIn((ConverterRegistry) conversionService); - this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, builder.bookmarkManager); + Optional.ofNullable(builder.neo4jConversions) + .orElseGet(Neo4jConversions::new) + .registerConvertersIn((ConverterRegistry) this.conversionService); + this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, + builder.bookmarkManager); } @Override - public Mono getQueryRunner(Mono databaseSelection, Mono userSelection) { + public Mono getQueryRunner(Mono databaseSelection, + Mono userSelection) { return databaseSelection.zipWith(userSelection) - .flatMap(targetDatabaseAndUser -> - ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2()) - .map(ReactiveQueryRunner.class::cast) - .zipWith(Mono.just(bookmarkManager.resolve().getBookmarks())) - .switchIfEmpty(Mono.fromSupplier(() -> { - Collection lastBookmarks = bookmarkManager.resolve().getBookmarks(); - return Tuples.of(driver.session(ReactiveSession.class, Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2())), lastBookmarks); - }))) - .map(t -> new DelegatingQueryRunner(t.getT1(), t.getT2(), bookmarkManager.resolve()::updateBookmarks)); + .flatMap(targetDatabaseAndUser -> ReactiveNeo4jTransactionManager + .retrieveReactiveTransaction(this.driver, targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2()) + .map(ReactiveQueryRunner.class::cast) + .zipWith(Mono.just(this.bookmarkManager.resolve().getBookmarks())) + .switchIfEmpty(Mono.fromSupplier(() -> { + Collection lastBookmarks = this.bookmarkManager.resolve().getBookmarks(); + return Tuples.of( + this.driver.session(ReactiveSession.class, + Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, + targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2())), + lastBookmarks); + }))) + .map(t -> new DelegatingQueryRunner(t.getT1(), t.getT2(), this.bookmarkManager.resolve()::updateBookmarks)); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - bookmarkManager.setApplicationContext(applicationContext); + this.bookmarkManager.setApplicationContext(applicationContext); } - private static class DelegatingQueryRunner implements ReactiveQueryRunner { + Mono doInQueryRunnerForMono(Mono databaseSelection, Mono userSelection, + Function> func) { - private final ReactiveQueryRunner delegate; - private final Collection usedBookmarks; - private final BiConsumer, Collection> newBookmarkConsumer; - - private DelegatingQueryRunner(ReactiveQueryRunner delegate, Collection lastBookmarks, BiConsumer, Collection> newBookmarkConsumer) { - this.delegate = delegate; - this.usedBookmarks = lastBookmarks; - this.newBookmarkConsumer = newBookmarkConsumer; - } - - Mono close() { - - // We're only going to close sessions we have acquired inside the client, not something that - // has been retrieved from the tx manager. - if (this.delegate instanceof ReactiveSession session) { - return Mono.fromDirect(session.close()).then().doOnSuccess(signal -> - this.newBookmarkConsumer.accept(usedBookmarks, session.lastBookmarks())); - } - - return Mono.empty(); - } - - @Override - public Publisher run(String query, Value parameters) { - return delegate.run(query, parameters); - } - - @Override - public Publisher run(String query, Map parameters) { - return delegate.run(query, parameters); - } - - @Override - public Publisher run(String query, Record parameters) { - return delegate.run(query, parameters); - } - - @Override - public Publisher run(String query) { - return delegate.run(query); - } - - @Override - public Publisher run(Query query) { - return delegate.run(query); - } + return Mono.usingWhen(getQueryRunner(databaseSelection, userSelection), func, + runner -> ((DelegatingQueryRunner) runner).close()); } - Mono doInQueryRunnerForMono(Mono databaseSelection, Mono userSelection, Function> func) { + Flux doInStatementRunnerForFlux(Mono databaseSelection, Mono userSelection, + Function> func) { - return Mono.usingWhen(getQueryRunner(databaseSelection, userSelection), func, runner -> ((DelegatingQueryRunner) runner).close()); - } - - Flux doInStatementRunnerForFlux(Mono databaseSelection, Mono userSelection, Function> func) { - - return Flux.usingWhen(getQueryRunner(databaseSelection, userSelection), func, runner -> ((DelegatingQueryRunner) runner).close()); + return Flux.usingWhen(getQueryRunner(databaseSelection, userSelection), func, + runner -> ((DelegatingQueryRunner) runner).close()); } @Override @@ -184,9 +150,8 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - @Nullable - public ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider() { - return databaseSelectionProvider; + @Nullable public ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider() { + return this.databaseSelectionProvider; } private Mono resolveTargetDatabaseName(@Nullable String parameterTargetDatabase) { @@ -195,10 +160,10 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati if (value != null) { return Mono.just(DatabaseSelection.byName(value)); } - if (databaseSelectionProvider != null) { - return databaseSelectionProvider.getDatabaseSelection(); - } - return ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider().getDatabaseSelection(); + return Objects + .requireNonNullElseGet(this.databaseSelectionProvider, + ReactiveDatabaseSelectionProvider::getDefaultSelectionProvider) + .getDatabaseSelection(); } private Mono resolveUser(@Nullable String userName) { @@ -206,22 +171,91 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati if (StringUtils.hasText(userName)) { return Mono.just(UserSelection.impersonate(userName)); } - if (userSelectionProvider != null) { - return userSelectionProvider.getUserSelection(); + return Objects + .requireNonNullElseGet(this.userSelectionProvider, + ReactiveUserSelectionProvider::getDefaultSelectionProvider) + .getUserSelection(); + } + + /** + * Tries to convert the given {@link RuntimeException} into a + * {@link DataAccessException} but returns the original exception if the conversation + * failed. Thus allows safe re-throwing of the return value. + * @param ex the exception to translate + * @return any translated exception + */ + private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { + RuntimeException resolved = this.persistenceExceptionTranslator.translateExceptionIfPossible(ex); + return (resolved != null) ? resolved : ex; + } + + private static final class DelegatingQueryRunner implements ReactiveQueryRunner { + + private final ReactiveQueryRunner delegate; + + private final Collection usedBookmarks; + + private final BiConsumer, Collection> newBookmarkConsumer; + + private DelegatingQueryRunner(ReactiveQueryRunner delegate, Collection lastBookmarks, + BiConsumer, Collection> newBookmarkConsumer) { + this.delegate = delegate; + this.usedBookmarks = lastBookmarks; + this.newBookmarkConsumer = newBookmarkConsumer; } - return ReactiveUserSelectionProvider.getDefaultSelectionProvider().getUserSelection(); + + Mono close() { + + // We're only going to close sessions we have acquired inside the client, not + // something that + // has been retrieved from the tx manager. + if (this.delegate instanceof ReactiveSession session) { + return Mono.fromDirect(session.close()) + .then() + .doOnSuccess( + signal -> this.newBookmarkConsumer.accept(this.usedBookmarks, session.lastBookmarks())); + } + + return Mono.empty(); + } + + @Override + public Publisher run(String query, Value parameters) { + return this.delegate.run(query, parameters); + } + + @Override + public Publisher run(String query, Map parameters) { + return this.delegate.run(query, parameters); + } + + @Override + public Publisher run(String query, Record parameters) { + return this.delegate.run(query, parameters); + } + + @Override + public Publisher run(String query) { + return this.delegate.run(query); + } + + @Override + public Publisher run(Query query) { + return this.delegate.run(query); + } + } class DefaultRunnableSpec implements UnboundRunnableSpec, RunnableSpecBoundToDatabaseAndUser { private final Supplier cypherSupplier; + private final NamedParameters parameters = new NamedParameters(); + private Mono databaseSelection; private Mono userSelection; - private final NamedParameters parameters = new NamedParameters(); - DefaultRunnableSpec(Supplier cypherSupplier) { this.databaseSelection = resolveTargetDatabaseName(null); this.userSelection = resolveUser(null); @@ -256,20 +290,24 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati @Override public MappingSpec fetchAs(Class targetClass) { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, parameters, - new SingleValueMappingFunction<>(conversionService, targetClass)); + return new DefaultRecordFetchSpec<>(this.databaseSelection, this.userSelection, this.cypherSupplier, + this.parameters, + new SingleValueMappingFunction<>(DefaultReactiveNeo4jClient.this.conversionService, targetClass)); } @Override public RecordFetchSpec> fetch() { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, parameters, (t, r) -> r.asMap()); + return new DefaultRecordFetchSpec<>(this.databaseSelection, this.userSelection, this.cypherSupplier, + this.parameters, (t, r) -> r.asMap()); } @Override public Mono run() { - return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, this.parameters, (t, r) -> null).run(); + return new DefaultRecordFetchSpec<>(this.databaseSelection, this.userSelection, this.cypherSupplier, + this.parameters, (t, r) -> null) + .run(); } class DefaultOngoingBindSpec implements Neo4jClient.OngoingBindSpec { @@ -284,7 +322,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati @Override public RunnableSpec to(String name) { - DefaultRunnableSpec.this.parameters.add(name, value); + DefaultRunnableSpec.this.parameters.add(name, this.value); return DefaultRunnableSpec.this; } @@ -293,11 +331,13 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati Assert.notNull(binder, "Binder is required"); - return bindAll(binder.apply(value)); + return bindAll(binder.apply(this.value)); } + } class DefaultRunnableSpecBoundToDatabase implements RunnableSpecBoundToDatabase { + @Override public RunnableSpecBoundToDatabaseAndUser asUser(String aUser) { @@ -329,6 +369,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati public RunnableSpec bindAll(Map newParameters) { return DefaultRunnableSpec.this.bindAll(newParameters); } + } class DefaultRunnableSpecBoundToUser implements RunnableSpecBoundToUser { @@ -364,7 +405,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati public RunnableSpec bindAll(Map newParameters) { return DefaultRunnableSpec.this.bindAll(newParameters); } + } + } class DefaultRecordFetchSpec implements RecordFetchSpec, MappingSpec { @@ -379,7 +422,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati private BiFunction mappingFunction; - DefaultRecordFetchSpec(Mono databaseSelection, Mono userSelection, Supplier cypherSupplier, NamedParameters parameters, BiFunction mappingFunction) { + DefaultRecordFetchSpec(Mono databaseSelection, Mono userSelection, + Supplier cypherSupplier, NamedParameters parameters, + BiFunction mappingFunction) { this.databaseSelection = databaseSelection; this.userSelection = userSelection; @@ -389,7 +434,8 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati } @Override - public RecordFetchSpec mappedBy(@SuppressWarnings("HiddenField") BiFunction mappingFunction) { + public RecordFetchSpec mappedBy( + @SuppressWarnings("HiddenField") BiFunction mappingFunction) { this.mappingFunction = mappingFunction; return this; @@ -397,81 +443,79 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati Mono>> prepareStatement() { if (cypherLog.isDebugEnabled()) { - String cypher = cypherSupplier.get(); + String cypher = this.cypherSupplier.get(); cypherLog.debug(() -> String.format("Executing:%s%s", System.lineSeparator(), cypher)); - if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { - cypherLog.trace(() -> String.format("with parameters:%s%s", System.lineSeparator(), parameters)); + if (cypherLog.isTraceEnabled() && !this.parameters.isEmpty()) { + cypherLog + .trace(() -> String.format("with parameters:%s%s", System.lineSeparator(), this.parameters)); } } - return Mono.fromSupplier(cypherSupplier).zipWith(Mono.just(parameters.get())); + return Mono.fromSupplier(this.cypherSupplier).zipWith(Mono.just(this.parameters.get())); } Flux executeWith(Tuple2> t, ReactiveQueryRunner runner) { return Flux.usingWhen(Flux.from(runner.run(t.getT1(), t.getT2())), result -> Flux.from(result.records()).flatMap(r -> { - if (mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 && r.get(0).hasType(TypeSystem.getDefault().LIST())) { - return Flux.fromStream(r.get(0).asList(v -> ((SingleValueMappingFunction) mappingFunction).convertValue(v)).stream()); + if (this.mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 + && r.get(0).hasType(TypeSystem.getDefault().LIST())) { + return Flux.fromStream(r.get(0) + .asList(v -> ((SingleValueMappingFunction) this.mappingFunction).convertValue(v)) + .stream()); } - var item = mappingFunction.apply(TypeSystem.getDefault(), r); - return item == null ? Flux.empty() : Flux.just(item); - }), - result -> Flux.from(result.consume()).doOnNext(ResultSummaries::process)); + var item = this.mappingFunction.apply(TypeSystem.getDefault(), r); + return (item != null) ? Flux.just(item) : Flux.empty(); + }), result -> Flux.from(result.consume()).doOnNext(ResultSummaries::process)); } @Override public Mono one() { - return doInQueryRunnerForMono(databaseSelection, userSelection, - (runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty() - .onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException)); + return doInQueryRunnerForMono(this.databaseSelection, this.userSelection, + (runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)) + .singleOrEmpty() + .onErrorMap(RuntimeException.class, + DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException)); } @Override public Mono first() { - return doInQueryRunnerForMono(databaseSelection, userSelection, + return doInQueryRunnerForMono(this.databaseSelection, this.userSelection, runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next()) - .onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); + .onErrorMap(RuntimeException.class, + DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); } @Override public Flux all() { - return doInStatementRunnerForFlux(databaseSelection, userSelection, + return doInStatementRunnerForFlux(this.databaseSelection, this.userSelection, runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner))) - .onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); + .onErrorMap(RuntimeException.class, + DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); } Mono run() { - return doInQueryRunnerForMono(databaseSelection, userSelection, runner -> prepareStatement() - .flatMap(t -> Flux.from(runner.run(t.getT1(), t.getT2())).single()) - .flatMap(rxResult -> Flux.from(rxResult.consume()).single().map(ResultSummaries::process))) - .onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); + return doInQueryRunnerForMono(this.databaseSelection, this.userSelection, + runner -> prepareStatement().flatMap(t -> Flux.from(runner.run(t.getT1(), t.getT2())).single()) + .flatMap(rxResult -> Flux.from(rxResult.consume()).single().map(ResultSummaries::process))) + .onErrorMap(RuntimeException.class, + DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException); } - } - /** - * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original - * exception if the conversation failed. Thus allows safe re-throwing of the return value. - * - * @param ex the exception to translate - * @return Any translated exception - */ - private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { - RuntimeException resolved = persistenceExceptionTranslator.translateExceptionIfPossible(ex); - return resolved == null ? ex : resolved; } class DefaultRunnableDelegation implements RunnableDelegation, OngoingDelegation { private final Function> callback; - private Mono databaseSelection; private final Mono userSelection; + private Mono databaseSelection; + DefaultRunnableDelegation(Function> callback) { this.callback = callback; this.databaseSelection = resolveTargetDatabaseName(null); @@ -488,7 +532,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati @Override public Mono run() { - return doInQueryRunnerForMono(databaseSelection, userSelection, callback); + return doInQueryRunnerForMono(this.databaseSelection, this.userSelection, this.callback); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveUserSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveUserSelectionProvider.java new file mode 100644 index 000000000..b645e92c3 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveUserSelectionProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011-2025 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.core; + +import reactor.core.publisher.Mono; + +/** + * Default implementation of {@link ReactiveUserSelectionProvider}. + * + * @author Michael J. Simons + * @since 6.2 + */ +enum DefaultReactiveUserSelectionProvider implements ReactiveUserSelectionProvider { + + INSTANCE; + + @Override + public Mono getUserSelection() { + return Mono.just(UserSelection.connectedUser()); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultUserSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/DefaultUserSelectionProvider.java new file mode 100644 index 000000000..5244ede3b --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultUserSelectionProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011-2025 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.core; + +/** + * Default implementation of {@link ReactiveUserSelectionProvider}. + * + * @author Michael J. Simons + * @since 6.2 + */ +enum DefaultUserSelectionProvider implements UserSelectionProvider { + + INSTANCE; + + @Override + public UserSelection getUserSelection() { + return UserSelection.connectedUser(); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java index 560fe6a90..3401cde4c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java +++ b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java @@ -25,11 +25,13 @@ import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate; + import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.NodeDescription; /** - * Decorator for an ongoing update statement that removes obsolete dynamic labels and adds new ones. + * Decorator for an ongoing update statement that removes obsolete dynamic labels and adds + * new ones. * * @author Michael J. Simons */ @@ -40,9 +42,11 @@ final class DynamicLabels implements UnaryOperator { private final Node rootNode; private final List oldLabels; + private final List newLabels; - DynamicLabels(@Nullable NodeDescription nodeDescription, Collection oldLabels, @Nullable Collection newLabels) { + DynamicLabels(@Nullable NodeDescription nodeDescription, Collection oldLabels, + @Nullable Collection newLabels) { this.oldLabels = new ArrayList<>(oldLabels); this.newLabels = (newLabels != null) ? new ArrayList<>(newLabels) : List.of(); this.rootNode = Cypher.anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); @@ -52,12 +56,14 @@ final class DynamicLabels implements UnaryOperator { public OngoingMatchAndUpdate apply(OngoingMatchAndUpdate ongoingMatchAndUpdate) { OngoingMatchAndUpdate decoratedMatchAndUpdate = ongoingMatchAndUpdate; - if (!oldLabels.isEmpty()) { - decoratedMatchAndUpdate = decoratedMatchAndUpdate.remove(rootNode, oldLabels.toArray(new String[0])); + if (!this.oldLabels.isEmpty()) { + decoratedMatchAndUpdate = decoratedMatchAndUpdate.remove(this.rootNode, + this.oldLabels.toArray(new String[0])); } - if (!newLabels.isEmpty()) { - decoratedMatchAndUpdate = decoratedMatchAndUpdate.set(rootNode, newLabels.toArray(new String[0])); + if (!this.newLabels.isEmpty()) { + decoratedMatchAndUpdate = decoratedMatchAndUpdate.set(this.rootNode, this.newLabels.toArray(new String[0])); } return decoratedMatchAndUpdate; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java index cd5b72b79..bce76d092 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java @@ -23,15 +23,17 @@ import java.util.Optional; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; /** - * {@link FluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style. + * {@link FluentFindOperation} allows creation and execution of Neo4j find operations in a + * fluent API style. *

- * The starting {@literal domainType} is used for mapping the query provided via {@code by} into the - * Neo4j specific representation. By default, the originating {@literal domainType} is also used for mapping back the - * result. However, it is possible to define a different {@literal returnType} via - * {@code as} to mapping the result. + * The starting {@literal domainType} is used for mapping the query provided via + * {@code by} into the Neo4j specific representation. By default, the originating + * {@literal domainType} is also used for mapping back the result. However, it is possible + * to define a different {@literal returnType} via {@code as} to mapping the result. * * @author Michael Simons * @since 6.1 @@ -41,15 +43,16 @@ public interface FluentFindOperation { /** * Start creating a find operation for the given {@literal domainType}. - * * @param domainType must not be {@literal null}. + * @param the domain tyoe * @return new instance of {@link ExecutableFind}. * @throws IllegalArgumentException if domainType is {@literal null}. */ ExecutableFind find(Class domainType); /** - * Trigger find execution by calling one of the terminating methods from a state where no query is yet defined. + * Trigger find execution by calling one of the terminating methods from a state where + * no query is yet defined. * * @param returned type */ @@ -57,10 +60,10 @@ public interface FluentFindOperation { /** * Get all matching elements. - * * @return never {@literal null}. */ List all(); + } /** @@ -72,9 +75,9 @@ public interface FluentFindOperation { /** * Get exactly zero or one result. - * * @return {@link Optional#empty()} if no match found. - * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more + * than one match found. */ default Optional one() { return Optional.ofNullable(oneValue()); @@ -82,12 +85,12 @@ public interface FluentFindOperation { /** * Get exactly zero or one result. - * * @return {@literal null} if no match found. - * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more + * than one match found. */ - @Nullable - T oneValue(); + @Nullable T oneValue(); + } /** @@ -99,27 +102,26 @@ public interface FluentFindOperation { /** * Set the filter query to be used. - * * @param query must not be {@literal null}. - * @param parameter Optional parameter map + * @param parameter an optional parameter map * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. */ TerminatingFind matching(String query, Map parameter); /** - * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code - * and we actively discourage using this method. - * - * @param queryFragmentsAndParameters Encapsulated query fragments and parameters as created by the repository abstraction. + * Creates an executable query based on fragments and parameters. Hardly useful + * outside framework-code and we actively discourage using this method. + * @param queryFragmentsAndParameters encapsulated query fragments and parameters + * as created by the repository abstraction * @return new instance of {@link TerminatingFind}. - * @throws IllegalArgumentException if queryFragmentsAndParameters is {@literal null}. + * @throws IllegalArgumentException if queryFragmentsAndParameters is + * {@literal null}. */ TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters); /** * Set the filter query to be used. - * * @param query must not be {@literal null}. * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. @@ -130,9 +132,9 @@ public interface FluentFindOperation { /** * Set the filter {@link Statement statement} to be used. - * * @param statement must not be {@literal null}. - * @param parameter Will be merged with parameters in the statement. Parameters in {@code parameter} have precedence. + * @param parameter will be merged with parameters in the statement. Parameters in + * {@code parameter} have precedence * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if statement is {@literal null}. */ @@ -140,7 +142,6 @@ public interface FluentFindOperation { /** * Set the filter {@link Statement statement} to be used. - * * @param statement must not be {@literal null}. * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if criteria is {@literal null}. @@ -148,6 +149,7 @@ public interface FluentFindOperation { default TerminatingFind matching(Statement statement) { return matching(statement, Collections.emptyMap()); } + } /** @@ -160,13 +162,13 @@ public interface FluentFindOperation { /** * Define the target type fields should be mapped to.
* Skip this step if you are anyway only interested in the original domain type. - * * @param resultType must not be {@literal null}. - * @param result type. + * @param result type. * @return new instance of {@link FindWithProjection}. * @throws IllegalArgumentException if resultType is {@literal null}. */ FindWithQuery as(Class resultType); + } /** @@ -175,5 +177,7 @@ public interface FluentFindOperation { * @param returned type */ interface ExecutableFind extends FindWithProjection { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentNeo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/FluentNeo4jOperations.java index 4cf4e9856..e10d17864 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentNeo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentNeo4jOperations.java @@ -18,13 +18,13 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; /** - * An additional interface accompanying the {@link Neo4jOperations} and adding a couple of fluent operations, especially - * around finding and projecting things. + * An additional interface accompanying the {@link Neo4jOperations} and adding a couple of + * fluent operations, especially around finding and projecting things. * * @author Michael J. Simons - * @soundtrack Ozzy Osbourne - Ordinary Man * @since 6.1 */ @API(status = API.Status.STABLE, since = "6.1") public interface FluentNeo4jOperations extends FluentFindOperation, FluentSaveOperation { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java index 57a69344d..206270eb3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java @@ -21,6 +21,7 @@ import java.util.Map; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; @@ -43,19 +44,32 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableFindSupport<>(template, domainType, domainType, null, Collections.emptyMap()); + return new ExecutableFindSupport<>(this.template, domainType, domainType, null, Collections.emptyMap()); + } + + @Override + public ExecutableSave save(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null"); + + return new ExecutableSaveSupport<>(this.template, domainType); } private static class ExecutableFindSupport implements ExecutableFind, FindWithProjection, FindWithQuery, TerminatingFind { private final Neo4jTemplate template; + private final Class domainType; + private final Class returnType; + @Nullable private final String query; + @Nullable private final Map parameters; + @Nullable private final QueryFragmentsAndParameters queryFragmentsAndParameters; @@ -69,7 +83,8 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe this.queryFragmentsAndParameters = null; } - ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { + ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, + @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -84,7 +99,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe Assert.notNull(returnType, "ReturnType must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); + return new ExecutableFindSupport<>(this.template, this.domainType, returnType, this.query, this.parameters); } @Override @@ -92,7 +107,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe public TerminatingFind matching(String query, Map parameters) { Assert.notNull(query, "Query must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); + return new ExecutableFindSupport<>(this.template, this.domainType, this.returnType, query, parameters); } @Override @@ -101,18 +116,18 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe Assert.notNull(queryFragmentsAndParameters, "Query fragments must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters); + return new ExecutableFindSupport<>(this.template, this.domainType, this.returnType, + queryFragmentsAndParameters); } @Override public TerminatingFind matching(Statement statement, Map parameter) { - return matching(template.render(statement), TemplateSupport.mergeParameters(statement, parameter)); + return matching(this.template.render(statement), TemplateSupport.mergeParameters(statement, parameter)); } @Override - @Nullable - public T oneValue() { + @Nullable public T oneValue() { List result = doFind(TemplateSupport.FetchType.ONE); if (result.isEmpty()) { @@ -127,21 +142,16 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe } private List doFind(TemplateSupport.FetchType fetchType) { - return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters); + return this.template.doFind(this.query, this.parameters, this.domainType, this.returnType, fetchType, + this.queryFragmentsAndParameters); } - } - @Override - public ExecutableSave save(Class domainType) { - - Assert.notNull(domainType, "DomainType must not be null"); - - return new ExecutableSaveSupport<>(this.template, domainType); } private static class ExecutableSaveSupport

implements ExecutableSave
{ private final Neo4jTemplate template; + private final Class
domainType; ExecutableSaveSupport(Neo4jTemplate template, Class
domainType) { @@ -166,7 +176,9 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe } private List doSave(Iterable instances) { - return template.doSave(instances, domainType); + return this.template.doSave(instances, this.domainType); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentSaveOperation.java b/src/main/java/org/springframework/data/neo4j/core/FluentSaveOperation.java index 1051cc8c5..fcc9e2bbc 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentSaveOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentSaveOperation.java @@ -20,12 +20,14 @@ import java.util.List; import org.apiguardian.api.API; /** - * {@link FluentSaveOperation} allows creation and execution of Neo4j save operations in a fluent API style. It - * is designed to be used together with the {@link FluentFindOperation fluent find operations}. + * {@link FluentSaveOperation} allows creation and execution of Neo4j save operations in a + * fluent API style. It is designed to be used together with the + * {@link FluentFindOperation fluent find operations}. *

- * Both interfaces provide a way to specify a pair of two types: A domain type and a result (projected) type. - * The fluent save operations are mainly used with DTO based projections. Closed interface projections won't be that - * helpful when you received them via {@link FluentFindOperation fluent find operations} as they won't be modifiable. + * Both interfaces provide a way to specify a pair of two types: A domain type and a + * result (projected) type. The fluent save operations are mainly used with DTO based + * projections. Closed interface projections won't be that helpful when you received them + * via {@link FluentFindOperation fluent find operations} as they won't be modifiable. * * @author Michael J. Simons * @author Gerrit Meier @@ -36,36 +38,43 @@ public interface FluentSaveOperation { /** * Start creating a save operation for the given {@literal domainType}. - * * @param domainType must not be {@literal null}. + * @param the type of the domain type * @return new instance of {@link ExecutableSave}. * @throws IllegalArgumentException if domainType is {@literal null}. */ ExecutableSave save(Class domainType); /** - * After the domain type has been specified, related projections or instances of the domain type can be saved. + * After the domain type has been specified, related projections or instances of the + * domain type can be saved. * * @param

the domain type */ interface ExecutableSave
{ /** - * @param instance The instance to be saved - * @param The type of the instance passed to this method. It should be the same as the domain type before - * or a projection of the domain type. If they are not related, the results may be undefined. - * @return The saved instance, can also be a new object, so you are recommended to use this instance after - * the save operation + * Saves exactly one instance. + * @param instance the instance to be saved + * @param the type of the instance passed to this method. It should be the + * same as the domain type before or a projection of the domain type. If they are + * not related, the results may be undefined + * @return the saved instance, can also be a new object, so you are recommended to + * use this instance after the save operation */ T one(T instance); /** - * @param instances The instances to be saved - * @param The type of the instances passed to this method. It should be the same as the domain type before - * or a projection of the domain type. If they are not related, the results may be undefined. - * @return The saved instances, can also be a new objects, so you are recommended to use those instances - * after the save operation + * Saves several instances. + * @param instances the instances to be saved + * @param the type of the instances passed to this method. It should be the + * same as the domain type before or a projection of the domain type. If they are + * not related, the results may be undefined + * @return the saved instances, can also be a new objects, so you are recommended + * to use those instances after the save operation */ List all(Iterable instances); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java b/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java index 7a5b9bca3..b4e48a8a8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/KPropertyFilterSupport.java @@ -19,21 +19,25 @@ import java.util.Collection; import java.util.Collections; import java.util.function.Predicate; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + import org.springframework.core.KotlinDetector; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; -import kotlin.reflect.KParameter; -import kotlin.reflect.jvm.ReflectJvmMapping; - /** + * Kotlin specific supported functions. + * * @author Michael J. Simons */ final class KPropertyFilterSupport { + private KPropertyFilterSupport() { + } + /** - * Determines all required constructor args for a Kotlin type - * + * Determines all required constructor args for a Kotlin type. * @param type the type for which required constructor args must be determined * @return a list of property names that need to be fetched */ @@ -52,12 +56,11 @@ final class KPropertyFilterSupport { return Collections.emptyList(); } - return preferredConstructor.getParameters().stream() - .filter(Predicate.not(KParameter::isOptional)) - .map(KParameter::getName) - .toList(); + return preferredConstructor.getParameters() + .stream() + .filter(Predicate.not(KParameter::isOptional)) + .map(KParameter::getName) + .toList(); } - private KPropertyFilterSupport() { - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index ba9a0c758..0b3e5ed46 100644 --- a/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -27,12 +27,14 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.driver.Value; + import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.MapValueWrapper; /** + * Support for named query parameters. + * * @author Michael J. Simons - * @soundtrack Bananafishbones - Viva Conputa * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -40,71 +42,6 @@ final class NamedParameters { private final Map parameters = new HashMap<>(); - /** - * Adds all of the values contained in {@code newParameters} to this list of named parameters. - * - * @param newParameters Additional parameters to add - * @throws IllegalStateException when any value in {@code newParameters} exists under the same name in the current - * parameters. - */ - void addAll(Map newParameters) { - newParameters.forEach(this::add); - } - - /** - * Adds a new parameter under the key {@code name} with the value {@code value}. - * - * @param name The name of the new parameter - * @param value The value of the new parameter - * @throws IllegalStateException when a parameter with the given name already exists - */ - @SuppressWarnings("unchecked") - void add(String name, @Nullable Object value) { - - if (this.parameters.containsKey(name)) { - Object previousValue = this.parameters.get(name); - throw new IllegalArgumentException(String.format( - "Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'", - name, previousValue == null ? "null" : previousValue.toString(), value == null ? "null" : value.toString())); - } - - if (Constants.NAME_OF_PROPERTIES_PARAM.equals(name) && value != null) { - this.parameters.put(name, unwrapMapValueWrapper((Map) value)); - } else if (Constants.NAME_OF_RELATIONSHIP_LIST_PARAM.equals(name) && value != null) { - this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List>) value)); - } else if (Constants.NAME_OF_ENTITY_LIST_PARAM.equals(name) && value != null) { - this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List>) value)); - } else { - this.parameters.put(name, value); - } - } - - @SuppressWarnings("unchecked") - private List> unwrapMapValueWrapperInListOfEntities(List> entityList) { - boolean requiresChange = entityList.stream().anyMatch( - entity -> - entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM) && - ((Map) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)).values().stream() - .anyMatch(MapValueWrapper.class::isInstance) - ); - - if (!requiresChange) { - return entityList; - } - - List> newEntityList = new ArrayList<>(entityList.size()); - for (Map entity : entityList) { - if (entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM)) { - Map newEntity = new HashMap<>(entity); - newEntity.put(Constants.NAME_OF_PROPERTIES_PARAM, unwrapMapValueWrapper((Map) entity.get(Constants.NAME_OF_PROPERTIES_PARAM))); - newEntityList.add(newEntity); - } else { - newEntityList.add(entity); - } - } - return newEntityList; - } - private static Map unwrapMapValueWrapper(Map properties) { if (properties.values().stream().noneMatch(MapValueWrapper.class::isInstance)) { @@ -116,45 +53,119 @@ final class NamedParameters { if (v instanceof MapValueWrapper) { Value mapValue = ((MapValueWrapper) v).getMapValue(); mapValue.keys().forEach(k2 -> newProperties.put(k2, mapValue.get(k2))); - } else { + } + else { newProperties.put(k, v); } }); return newProperties; } - /** - * @return An unmodifiable copy of this list's values. - */ - Map get() { - return Collections.unmodifiableMap(parameters); - } - - public boolean isEmpty() { - return parameters.isEmpty(); - } - - @Override - public String toString() { - return parameters.entrySet().stream().map(e -> String.format(":param %s => %s", e.getKey(), formatValue(e.getValue()))) - .collect(Collectors.joining(System.lineSeparator())); - } - - @Nullable - private static String formatValue(Object value) { + @Nullable private static String formatValue(Object value) { if (value == null) { return null; - } else if (value instanceof String) { + } + else if (value instanceof String) { return Cypher.quote((String) value); - } else if (value instanceof Map) { - return ((Map) value).entrySet().stream() - .map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue()))).collect( - Collectors.joining(", ", "{", "}")); - } else if (value instanceof Collection) { - return ((Collection) value).stream().map(NamedParameters::formatValue).collect( - Collectors.joining(", ", "[", "]")); + } + else if (value instanceof Map) { + return ((Map) value).entrySet() + .stream() + .map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue()))) + .collect(Collectors.joining(", ", "{", "}")); + } + else if (value instanceof Collection) { + return ((Collection) value).stream() + .map(NamedParameters::formatValue) + .collect(Collectors.joining(", ", "[", "]")); } return value.toString(); } + + /** + * Adds all the values contained in {@code newParameters} to this list of named + * parameters. + * @param newParameters additional parameters to add + * @throws IllegalStateException when any value in {@code newParameters} exists under + * the same name in the current parameters. + */ + void addAll(Map newParameters) { + newParameters.forEach(this::add); + } + + /** + * Adds a new parameter under the key {@code name} with the value {@code value}. + * @param name the name of the new parameter + * @param value the value of the new parameter + * @throws IllegalStateException when a parameter with the given name already exists + */ + @SuppressWarnings("unchecked") + void add(String name, @Nullable Object value) { + + if (this.parameters.containsKey(name)) { + Object previousValue = this.parameters.get(name); + throw new IllegalArgumentException(String.format( + "Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'", + name, (previousValue != null) ? previousValue.toString() : "null", + (value != null) ? value.toString() : "null")); + } + + if (Constants.NAME_OF_PROPERTIES_PARAM.equals(name) && value != null) { + this.parameters.put(name, unwrapMapValueWrapper((Map) value)); + } + else if (Constants.NAME_OF_RELATIONSHIP_LIST_PARAM.equals(name) && value != null) { + this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List>) value)); + } + else if (Constants.NAME_OF_ENTITY_LIST_PARAM.equals(name) && value != null) { + this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List>) value)); + } + else { + this.parameters.put(name, value); + } + } + + @SuppressWarnings("unchecked") + private List> unwrapMapValueWrapperInListOfEntities(List> entityList) { + boolean requiresChange = entityList.stream() + .anyMatch(entity -> entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM) + && ((Map) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)).values() + .stream() + .anyMatch(MapValueWrapper.class::isInstance)); + + if (!requiresChange) { + return entityList; + } + + List> newEntityList = new ArrayList<>(entityList.size()); + for (Map entity : entityList) { + if (entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM)) { + Map newEntity = new HashMap<>(entity); + newEntity.put(Constants.NAME_OF_PROPERTIES_PARAM, + unwrapMapValueWrapper((Map) entity.get(Constants.NAME_OF_PROPERTIES_PARAM))); + newEntityList.add(newEntity); + } + else { + newEntityList.add(entity); + } + } + return newEntityList; + } + + Map get() { + return Collections.unmodifiableMap(this.parameters); + } + + boolean isEmpty() { + return this.parameters.isEmpty(); + } + + @Override + public String toString() { + return this.parameters.entrySet() + .stream() + .map(e -> String.format(":param %s => %s", e.getKey(), formatValue(e.getValue()))) + .collect(Collectors.joining(System.lineSeparator())); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 88e59555d..4bdf4e529 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -32,6 +32,7 @@ import org.neo4j.driver.QueryRunner; import org.neo4j.driver.Record; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -47,12 +48,19 @@ import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; public interface Neo4jClient { /** - * This is a public API introduced to turn the logging of the infamous warning back on. - * {@code The query used a deprecated function: `id`.} + * This is a public API introduced to turn the logging of the infamous warning back + * on. {@code The query used a deprecated function: `id`.} */ AtomicBoolean SUPPRESS_ID_DEPRECATIONS = new AtomicBoolean(true); + /** + * All Cypher statements executed will be logged here. + */ LogAccessor cypherLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher")); + + /** + * Some methods of the {@link Neo4jClient} will be logged here. + */ LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jClient.class)); static Neo4jClient create(Driver driver) { @@ -70,12 +78,320 @@ public interface Neo4jClient { return new Builder(driver); } + /** + * This is a utility method to verify and sanitize a database name. + * @param databaseName the database name to verify and sanitize + * @return a possibly trimmed name of the database + * @throws IllegalArgumentException when the database name is not allowed with the + * underlying driver. + */ + @Nullable static String verifyDatabaseName(@Nullable String databaseName) { + + String newTargetDatabase = (databaseName != null) ? databaseName.trim() : null; + if (newTargetDatabase != null && newTargetDatabase.isEmpty()) { + throw new IllegalDatabaseNameException(newTargetDatabase); + } + return newTargetDatabase; + } + + /** + * Retrieves a query runner matching the plain Neo4j Java Driver api bound to Spring + * transactions. + * @return a managed query runner + * @since 6.2 + * @see #getQueryRunner(DatabaseSelection, UserSelection) + */ + default QueryRunner getQueryRunner() { + return getQueryRunner(DatabaseSelection.undecided()); + } + + /** + * Retrieves a query runner matching the plain Neo4j Java Driver api bound to Spring + * transactions configured to use a specific database. + * @param databaseSelection the database to use + * @return a managed query runner + * @since 6.2 + * @see #getQueryRunner(DatabaseSelection, UserSelection) + */ + default QueryRunner getQueryRunner(DatabaseSelection databaseSelection) { + return getQueryRunner(databaseSelection, UserSelection.connectedUser()); + } + + /** + * Retrieves a query runner that will participate in ongoing Spring transactions + * (either in declarative (implicit via {@code @Transactional}) or in programmatically + * (explicit via transaction template) ones). This runner can be used with the + * Cypher-DSL for example. If the client cannot retrieve an ongoing Spring + * transaction, this runner will use auto-commit semantics. + * @param databaseSelection the target database + * @param asUser as an impersonated user. Requires Neo4j 4.4 and Driver 4.4 + * @return a managed query runner + * @since 6.2 + */ + QueryRunner getQueryRunner(DatabaseSelection databaseSelection, UserSelection asUser); + + /** + * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether + * it's a match, merge, create or removal of things. + * @param cypher the cypher code that shall be executed + * @return a runnable query specification + */ + UnboundRunnableSpec query(String cypher); + + /** + * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at + * this point whether it's a match, merge, create or removal of things. The supplier + * can be an arbitrary Supplier that may provide a DSL for generating the Cypher + * statement. + * @param cypherSupplier a supplier of arbitrary Cypher code + * @return a runnable query specification + */ + UnboundRunnableSpec query(Supplier cypherSupplier); + + /** + * Delegates interaction with the default database to the given callback. + * @param callback a function receiving a statement runner for database interaction + * that can optionally return a result + * @param the type of the result being produced + * @return a single result object or an empty optional if the callback didn't produce + * a result + */ + OngoingDelegation delegateTo(Function> callback); + + /** + * Returns the assigned database selection provider. + * @return the database selection provider - can be null + */ + @Nullable DatabaseSelectionProvider getDatabaseSelectionProvider(); + + /** + * Contract for a runnable query that can be either run returning its result, run + * without results or be parameterized. + * + * @since 6.0 + */ + interface RunnableSpec extends BindSpec { + + /** + * Create a mapping for each record return to a specific type. + * @param targetClass the class each record should be mapped to + * @param the type of the class + * @return a mapping spec that allows specifying a mapping function + */ + MappingSpec fetchAs(Class targetClass); + + /** + * Fetch all records mapped into generic maps. + * @return a fetch specification that maps into generic maps + */ + RecordFetchSpec> fetch(); + + /** + * Execute the query and discard the results. It returns the drivers result + * summary, including various counters and other statistics. + * @return the native summary of the query + */ + ResultSummary run(); + + } + + /** + * Contract for a runnable query specification which still can be bound to a specific + * database and an impersonated user. + * + * @since 6.2 + */ + interface UnboundRunnableSpec extends RunnableSpec { + + /** + * Pins the previously defined query to a specific database. A value of + * {@literal null} chooses the default database. The empty string {@literal ""} is + * not permitted. + * @param targetDatabase selected database to use. A {@literal null} value + * indicates the default database. + * @return a runnable query specification that is now bound to a given database + */ + RunnableSpecBoundToDatabase in(String targetDatabase); + + /** + * Pins the previously defined query to an impersonated user. A value of + * {@literal null} chooses the user owning the physical connection. The empty + * string {@literal ""} is not permitted. + * @param asUser the name of the user to impersonate. A {@literal null} value + * indicates the connected user + * @return a runnable query specification that is now bound to a given database. + */ + RunnableSpecBoundToUser asUser(String asUser); + + } + + /** + * Contract for a runnable query inside a dedicated database. + * + * @since 6.0 + */ + interface RunnableSpecBoundToDatabase extends RunnableSpec { + + RunnableSpecBoundToDatabaseAndUser asUser(String aUser); + + } + + /** + * Contract for a runnable query bound to a user to be impersonated. + * + * @since 6.2 + */ + interface RunnableSpecBoundToUser extends RunnableSpec { + + RunnableSpecBoundToDatabaseAndUser in(String aDatabase); + + } + + /** + * Combination of {@link RunnableSpecBoundToDatabase} and + * {@link RunnableSpecBoundToUser}, can't be bound any further. + * + * @since 6.2 + */ + interface RunnableSpecBoundToDatabaseAndUser extends RunnableSpec { + + } + + /** + * Contract for binding parameters to a query. + * + * @param this {@link BindSpec specs} own type + * @since 6.0 + */ + interface BindSpec> { + + /** + * Starts binding a value to a parameter. + * @param value the value to bind to a query + * @return an ongoing bind spec for specifying the name that {@code value} should + * be bound to or a binder function + * @param type of the value + */ + OngoingBindSpec bind(@Nullable T value); + + S bindAll(Map parameters); + + } + + /** + * Ongoing bind specification. + * + * @param this {@link OngoingBindSpec specs} own type + * @param binding value type + * @since 6.0 + */ + interface OngoingBindSpec> { + + /** + * Bind one convertible object to the given name. + * @param name the named parameter to bind the value to + * @return the bind specification itself for binding more values or execution + */ + S to(String name); + + /** + * Use a binder function for the previously defined value. + * @param binder the binder function to create a map of parameters from the given + * value + * @return the bind specification itself for binding more values or execution + */ + S with(Function> binder); + + } + + /** + * Step for defining the mapping. + * + * @param the resulting type of this mapping + * @since 6.0 + */ + interface MappingSpec extends RecordFetchSpec { + + /** + * The mapping function is responsible to turn one record into one domain object. + * It will receive the record itself and in addition, the type system that the + * Neo4j Java-Driver used while executing the query. + * @param mappingFunction the mapping function used to create new domain objects + * @return a specification how to fetch one or more records. + */ + RecordFetchSpec mappedBy(BiFunction mappingFunction); + + } + + /** + * Final step that triggers fetching. + * + * @param the type to which the fetched records are eventually mapped + * @since 6.0 + */ + interface RecordFetchSpec { + + /** + * Fetches exactly one record and throws an exception if there are more entries. + * @return the one and only record + */ + Optional one(); + + /** + * Fetches only the first record. Returns an empty holder if there are no records. + * @return the first record if any + */ + Optional first(); + + /** + * Fetches all records. + * @return all records + */ + Collection all(); + + } + + /** + * A contract for an ongoing delegation in the selected database. + * + * @param the type of the returned value + * @since 6.0 + */ + interface OngoingDelegation extends RunnableDelegation { + + /** + * Runs the delegation in the given target database. + * @param targetDatabase selected database to use. A {@literal null} value + * indicates the default database. + * @return an ongoing delegation + */ + RunnableDelegation in(String targetDatabase); + + } + + /** + * A runnable delegation. + * + * @param the type that gets returned + * @since 6.0 + */ + interface RunnableDelegation { + + /** + * Runs the stored callback. + * @return the optional result of the callback that has been executed with the + * given database + */ + Optional run(); + + } + /** * A builder for {@link Neo4jClient Neo4j clients}. */ @API(status = API.Status.STABLE, since = "6.2") @SuppressWarnings("HiddenField") - class Builder { + final class Builder { final Driver driver; @@ -96,12 +412,13 @@ public interface Neo4jClient { } /** - * Configures the database selection provider. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. During runtime, it will be - * checked if a call is made for the same database when happening in a managed transaction. - * - * @param databaseSelectionProvider The database selection provider - * @return The builder + * Configures the database selection provider. Make sure to use the same instance + * as for a possible + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. + * During runtime, it will be checked if a call is made for the same database when + * happening in a managed transaction. + * @param databaseSelectionProvider the database selection provider + * @return the builder */ public Builder withDatabaseSelectionProvider(@Nullable DatabaseSelectionProvider databaseSelectionProvider) { this.databaseSelectionProvider = databaseSelectionProvider; @@ -109,12 +426,13 @@ public interface Neo4jClient { } /** - * Configures a provider for impersonated users. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. During runtime, it will be - * checked if a call is made for the same user when happening in a managed transaction. - * - * @param userSelectionProvider The provider for impersonated users - * @return The builder + * Configures a provider for impersonated users. Make sure to use the same + * instance as for a possible + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. + * During runtime, it will be checked if a call is made for the same user when + * happening in a managed transaction. + * @param userSelectionProvider the provider for impersonated users + * @return the builder */ public Builder withUserSelectionProvider(@Nullable UserSelectionProvider userSelectionProvider) { this.userSelectionProvider = userSelectionProvider; @@ -123,9 +441,9 @@ public interface Neo4jClient { /** * Configures the set of {@link Neo4jConversions} to use. - * - * @param neo4jConversions the set of conversions to use, can be {@literal null}, in this case the default set is used. - * @return The builder + * @param neo4jConversions the set of conversions to use, can be {@literal null}, + * in this case the default set is used. + * @return the builder * @since 6.3.3 */ public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) { @@ -134,12 +452,14 @@ public interface Neo4jClient { } /** - * Configures the {@link Neo4jBookmarkManager} to use. - * This should be the same instance as provided for the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager} - * respectively the {@link org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager}. - * - * @param bookmarkManager Neo4jBookmarkManager instance that is shared with the transaction manager. - * @return The builder + * Configures the {@link Neo4jBookmarkManager} to use. This should be the same + * instance as provided for the + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager} + * respectively the + * {@link org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager}. + * @param bookmarkManager the bookmark manager instance that is shared with the + * transaction manager + * @return the builder * @since 7.1.2 */ public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) { @@ -150,307 +470,17 @@ public interface Neo4jClient { public Neo4jClient build() { return new DefaultNeo4jClient(this); } + } /** - * @return A managed query runner - * @see #getQueryRunner(DatabaseSelection, UserSelection) - * @since 6.2 - */ - default QueryRunner getQueryRunner() { - return getQueryRunner(DatabaseSelection.undecided()); - } - - /** - * @return A managed query runner - * @see #getQueryRunner(DatabaseSelection, UserSelection) - * @since 6.2 - */ - default QueryRunner getQueryRunner(DatabaseSelection databaseSelection) { - return getQueryRunner(databaseSelection, UserSelection.connectedUser()); - } - - /** - * Retrieves a query runner that will participate in ongoing Spring transactions (either in declarative - * (implicit via {@code @Transactional}) or in programmatically (explicit via transaction template) ones). - * This runner can be used with the Cypher-DSL for example. - * If the client cannot retrieve an ongoing Spring transaction, this runner will use auto-commit semantics. + * Indicates an illegal database name and is not translated into a + * {@link org.springframework.dao.DataAccessException}. * - * @param databaseSelection The target database. - * @param asUser As an impersonated user. Requires Neo4j 4.4 and Driver 4.4 - * @return A managed query runner - * @since 6.2 - */ - QueryRunner getQueryRunner(DatabaseSelection databaseSelection, UserSelection asUser); - - /** - * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether it's a match, merge, create or - * removal of things. - * - * @param cypher The cypher code that shall be executed - * @return A runnable query specification. - */ - UnboundRunnableSpec query(String cypher); - - /** - * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, - * merge, create or removal of things. The supplier can be an arbitrary Supplier that may provide a DSL for generating - * the Cypher statement. - * - * @param cypherSupplier A supplier of arbitrary Cypher code - * @return A runnable query specification. - */ - UnboundRunnableSpec query(Supplier cypherSupplier); - - /** - * Delegates interaction with the default database to the given callback. - * - * @param callback A function receiving a statement runner for database interaction that can optionally return a - * result. - * @param The type of the result being produced - * @return A single result object or an empty optional if the callback didn't produce a result - */ - OngoingDelegation delegateTo(Function> callback); - - /** - * Returns the assigned database selection provider. - * - * @return The database selection provider - can be null - */ - @Nullable - DatabaseSelectionProvider getDatabaseSelectionProvider(); - - /** - * Contract for a runnable query that can be either run returning its result, run without results or be - * parameterized. - * - * @since 6.0 - */ - interface RunnableSpec extends BindSpec { - - /** - * Create a mapping for each record return to a specific type. - * - * @param targetClass The class each record should be mapped to - * @param The type of the class - * @return A mapping spec that allows specifying a mapping function. - */ - MappingSpec fetchAs(Class targetClass); - - /** - * Fetch all records mapped into generic maps - * - * @return A fetch specification that maps into generic maps. - */ - RecordFetchSpec> fetch(); - - /** - * Execute the query and discard the results. It returns the drivers result summary, including various counters and - * other statistics. - * - * @return The native summary of the query. - */ - ResultSummary run(); - } - - /** - * Contract for a runnable query specification which still can be bound to a specific database and an impersonated user. - * - * @since 6.2 - */ - interface UnboundRunnableSpec extends RunnableSpec { - - /** - * Pins the previously defined query to a specific database. A value of {@literal null} chooses the default - * database. The empty string {@literal ""} is not permitted. - * - * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. - * @return A runnable query specification that is now bound to a given database. - */ - RunnableSpecBoundToDatabase in(String targetDatabase); - - /** - * Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning - * the physical connection. The empty string {@literal ""} is not permitted. - * - * @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user. - * @return A runnable query specification that is now bound to a given database. - */ - RunnableSpecBoundToUser asUser(String asUser); - } - - /** - * Contract for a runnable query inside a dedicated database. - * - * @since 6.0 - */ - interface RunnableSpecBoundToDatabase extends RunnableSpec { - - RunnableSpecBoundToDatabaseAndUser asUser(String aUser); - } - - /** - * Contract for a runnable query bound to a user to be impersonated. - * - * @since 6.2 - */ - interface RunnableSpecBoundToUser extends RunnableSpec { - - RunnableSpecBoundToDatabaseAndUser in(String aDatabase); - } - - /** - * Combination of {@link RunnableSpecBoundToDatabase} and {@link RunnableSpecBoundToUser}, can't be - * bound any further. - * - * @since 6.2 - */ - interface RunnableSpecBoundToDatabaseAndUser extends RunnableSpec { - } - - /** - * Contract for binding parameters to a query. - * - * @param This {@link BindSpec specs} own type - * @since 6.0 - */ - interface BindSpec> { - - /** - * @param value The value to bind to a query - * @return An ongoing bind spec for specifying the name that {@code value} should be bound to or a binder function - */ - OngoingBindSpec bind(@Nullable T value); - - S bindAll(Map parameters); - } - - /** - * Ongoing bind specification. - * - * @param This {@link OngoingBindSpec specs} own type - * @param Binding value type - * @since 6.0 - */ - interface OngoingBindSpec> { - - /** - * Bind one convertible object to the given name. - * - * @param name The named parameter to bind the value to - * @return The bind specification itself for binding more values or execution. - */ - S to(String name); - - /** - * Use a binder function for the previously defined value. - * - * @param binder The binder function to create a map of parameters from the given value - * @return The bind specification itself for binding more values or execution. - */ - S with(Function> binder); - } - - /** - * @param The resulting type of this mapping - * @since 6.0 - */ - interface MappingSpec extends RecordFetchSpec { - - /** - * The mapping function is responsible to turn one record into one domain object. It will receive the record itself - * and in addition, the type system that the Neo4j Java-Driver used while executing the query. - * - * @param mappingFunction The mapping function used to create new domain objects - * @return A specification how to fetch one or more records. - */ - RecordFetchSpec mappedBy(BiFunction mappingFunction); - } - - /** - * @param The type to which the fetched records are eventually mapped - * @since 6.0 - */ - interface RecordFetchSpec { - - /** - * Fetches exactly one record and throws an exception if there are more entries. - * - * @return The one and only record. - */ - Optional one(); - - /** - * Fetches only the first record. Returns an empty holder if there are no records. - * - * @return The first record if any. - */ - Optional first(); - - /** - * Fetches all records. - * - * @return All records. - */ - Collection all(); - } - - /** - * A contract for an ongoing delegation in the selected database. - * - * @param The type of the returned value. - * @since 6.0 - */ - interface OngoingDelegation extends RunnableDelegation { - - /** - * Runs the delegation in the given target database. - * - * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. - * @return An ongoing delegation - */ - RunnableDelegation in(String targetDatabase); - } - - /** - * A runnable delegation. - * - * @param the type that gets returned - * @since 6.0 - */ - interface RunnableDelegation { - - /** - * Runs the stored callback. - * - * @return The optional result of the callback that has been executed with the given database. - */ - Optional run(); - } - - /** - * This is a utility method to verify and sanitize a database name. - * - * @param databaseName The database name to verify and sanitize - * @return A possibly trimmed name of the database. - * @throws IllegalArgumentException when the database name is not allowed with the underlying driver. - */ - @Nullable - static String verifyDatabaseName(@Nullable String databaseName) { - - String newTargetDatabase = databaseName == null ? null : databaseName.trim(); - if (newTargetDatabase != null && newTargetDatabase.isEmpty()) { - throw new IllegalDatabaseNameException(newTargetDatabase); - } - return newTargetDatabase; - } - - /** - * Indicates an illegal database name and is not translated into a {@link org.springframework.dao.DataAccessException}. * @since 6.1.5 */ @API(status = API.Status.STABLE, since = "6.1.5") - class IllegalDatabaseNameException extends IllegalArgumentException { + final class IllegalDatabaseNameException extends IllegalArgumentException { @Serial private static final long serialVersionUID = 3496326026855204643L; @@ -464,7 +494,9 @@ public interface Neo4jClient { @SuppressWarnings("unused") public String getIllegalDatabaseName() { - return illegalDatabaseName; + return this.illegalDatabaseName; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java index f6839382d..873dc98cd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java @@ -23,6 +23,7 @@ import java.util.function.BiPredicate; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; @@ -33,7 +34,6 @@ import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParamete * Specifies operations one can perform on a database, based on an Domain Type. * * @author Michael J. Simons - * @soundtrack Motörhead - We Are Motörhead * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -41,122 +41,114 @@ public interface Neo4jOperations { /** * Counts the number of entities of a given type. - * * @param domainType the type of the entities to be counted. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ long count(Class domainType); /** * Counts the number of entities of a given type. - * * @param statement the Cypher {@link Statement} that returns the count. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ long count(Statement statement); /** * Counts the number of entities of a given type. - * - * @param statement the Cypher {@link Statement} that returns the count. - * @param parameters Map of parameters. Must not be {@code null}. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @param statement the Cypher {@link Statement} that returns the count + * @param parameters map of parameters. Must not be {@code null} + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null} */ long count(Statement statement, Map parameters); /** * Counts the number of entities of a given type. - * * @param cypherQuery the Cypher query that returns the count. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ long count(String cypherQuery); /** * Counts the number of entities of a given type. - * - * @param cypherQuery the Cypher query that returns the count. - * @param parameters Map of parameters. Must not be {@code null}. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @param cypherQuery the Cypher query that returns the count + * @param parameters map of parameters. Must not be {@code null} + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null} */ long count(String cypherQuery, Map parameters); /** * Load all entities of a given type. - * - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null} */ List findAll(Class domainType); /** * Load all entities of a given type by executing given statement. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null} */ List findAll(Statement statement, Class domainType); /** * Load all entities of a given type by executing given statement with parameters. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null} + * @param parameters map of parameters. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null}. */ List findAll(Statement statement, Map parameters, Class domainType); /** * Load one entity of a given type by executing given statement with parameters. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null} + * @param parameters map of parameters. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null}. */ Optional findOne(Statement statement, Map parameters, Class domainType); /** * Load all entities of a given type by executing given statement. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null}. */ List findAll(String cypherQuery, Class domainType); /** * Load all entities of a given type by executing given statement with parameters. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null} + * @param parameters map of parameters. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ List findAll(String cypherQuery, Map parameters, Class domainType); /** * Load one entity of a given type by executing given statement with parameters. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null} + * @param parameters map of parameters. Must not be {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null} */ Optional findOne(String cypherQuery, Map parameters, Class domainType); /** * Load an entity from the database. - * * @param id the id of the entity to load. Must not be {@code null}. * @param domainType the type of the entity. Must not be {@code null}. * @param the type of the entity. @@ -166,27 +158,25 @@ public interface Neo4jOperations { /** * Load all entities of a given type that are identified by the given ids. - * - * @param ids of the entities identifying the entities to load. Must not be {@code null}. - * @param domainType the type of the entities. Must not be {@code null}. - * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @param ids of the entities identifying the entities to load. Must not be + * {@code null} + * @param domainType the type of the entities. Must not be {@code null} + * @param the type of the entities. Must not be {@code null} + * @return guaranteed to be not {@code null} */ List findAllById(Iterable ids, Class domainType); /** * Check if an entity for a given id exists in the database. - * * @param id the id of the entity to check. Must not be {@code null}. * @param domainType the type of the entity. Must not be {@code null}. * @param the type of the entity. - * @return If entity exists in the database, true, otherwise false. + * @return if entity exists in the database, true, otherwise false. */ boolean existsById(Object id, Class domainType); /** * Saves an instance of an entity, including all the related entities of the entity. - * * @param instance the entity to be saved. Must not be {@code null}. * @param the type of the entity. * @return the saved instance. @@ -194,41 +184,42 @@ public interface Neo4jOperations { T save(T instance); /** - * Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate - * as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include - * the association property as well (meaning the predicate must return {@literal true} for that property, too). + * Saves an instance of an entity, using the provided predicate to shape the stored + * graph. One can think of the predicate as a dynamic projection. If you want to save + * or update properties of associations (aka related nodes), you must include the + * association property as well (meaning the predicate must return {@literal true} for + * that property, too). *

- * Be careful when reusing the returned instance for further persistence operations, as it will most likely not be - * fully hydrated and without using a static or dynamic projection, you will most likely cause data loss. - * - * @param instance the entity to be saved. Must not be {@code null}. - * @param includeProperty A predicate to determine the properties to save. - * @param the type of the entity. + * Be careful when reusing the returned instance for further persistence operations, + * as it will most likely not be fully hydrated and without using a static or dynamic + * projection, you will most likely cause data loss. + * @param instance the entity to be saved. Must not be {@code null}. + * @param includeProperty a predicate to determine the properties to save. + * @param the type of the entity. * @return the saved instance. * @since 6.3 */ - @Nullable - default T saveAs(T instance, BiPredicate includeProperty) { + @Nullable default T saveAs(T instance, BiPredicate includeProperty) { throw new UnsupportedOperationException(); } /** - * Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}. - * + * Saves an instance of an entity, including the properties and relationship defined + * by the projected {@code resultType}. * @param instance the entity to be saved. Must not be {@code null}. + * @param resultType the projected type * @param the type of the entity. * @param the type of the projection to be used during save. * @return the saved, projected instance. * @since 6.1 */ - @Nullable - default R saveAs(T instance, Class resultType) { + @Nullable default R saveAs(T instance, Class resultType) { throw new UnsupportedOperationException(); } /** - * Saves several instances of an entity, including all the related entities of the entity. - * + * Saves several instances of an entity, including all the related entities of the + * entity. * @param instances the instances to be saved. Must not be {@code null}. * @param the type of the entity. * @return the saved instances. @@ -236,27 +227,31 @@ public interface Neo4jOperations { List saveAll(Iterable instances); /** - * Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate - * as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include - * the association property as well (meaning the predicate must return {@literal true} for that property, too). + * Saves several instances of an entity, using the provided predicate to shape the + * stored graph. One can think of the predicate as a dynamic projection. If you want + * to save or update properties of associations (aka related nodes), you must include + * the association property as well (meaning the predicate must return {@literal true} + * for that property, too). *

- * Be careful when reusing the returned instances for further persistence operations, as they will most likely not be - * fully hydrated and without using a static or dynamic projection, you will most likely cause data loss. - * - * @param instances the instances to be saved. Must not be {@code null}. - * @param includeProperty A predicate to determine the properties to save. - * @param the type of the entity. + * Be careful when reusing the returned instances for further persistence operations, + * as they will most likely not be fully hydrated and without using a static or + * dynamic projection, you will most likely cause data loss. + * @param instances the instances to be saved. Must not be {@code null}. + * @param includeProperty a predicate to determine the properties to save. + * @param the type of the entity. * @return the saved instances. * @since 6.3 */ - default List saveAllAs(Iterable instances, BiPredicate includeProperty) { + default List saveAllAs(Iterable instances, + BiPredicate includeProperty) { throw new UnsupportedOperationException(); } /** - * Saves an instance of an entity, including the properties and relationship defined by the project {@code resultType}. - * + * Saves an instance of an entity, including the properties and relationship defined + * by the project {@code resultType}. * @param instances the instances to be saved. Must not be {@code null}. + * @param resultType the projected type * @param the type of the entity. * @param the type of the projection to be used during save. * @return the saved, projected instance. @@ -268,18 +263,18 @@ public interface Neo4jOperations { /** * Deletes a single entity including all entities related to that entity. - * * @param id the id of the entity to be deleted. Must not be {@code null}. * @param domainType the type of the entity * @param the type of the entity. */ void deleteById(Object id, Class domainType); - void deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue); + void deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, + @Nullable Object versionValue); /** - * Deletes all entities with one of the given ids, including all entities related to that entity. - * + * Deletes all entities with one of the given ids, including all entities related to + * that entity. * @param ids the ids of the entities to be deleted. Must not be {@code null}. * @param domainType the type of the entity * @param the type of the entity. @@ -288,31 +283,31 @@ public interface Neo4jOperations { /** * Delete all entities of a given type. - * * @param domainType type of the entities to be deleted. Must not be {@code null}. */ void deleteAll(Class domainType); /** - * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and - * an optional mapping function, and turns it into an executable query. - * - * @param preparedQuery prepared query that should get converted to an executable query - * @param The type of the objects returned by this query. - * @return An executable query + * Takes a prepared query, containing all the information about the cypher template to + * be used, needed parameters and an optional mapping function, and turns it into an + * executable query. + * @param preparedQuery prepared query that should get converted to an executable + * query + * @param the type of the objects returned by this query. + * @return an executable query */ ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); /** * Create an executable query based on query fragment. - * * @param domainType domain class the executable query should return - * @param queryFragmentsAndParameters fragments and parameters to construct the query from - * @param The type of the objects returned by this query. - * @return An executable query + * @param queryFragmentsAndParameters fragments and parameters to construct the query + * from + * @param the type of the objects returned by this query. + * @return an executable query */ ExecutableQuery toExecutableQuery(Class domainType, - QueryFragmentsAndParameters queryFragmentsAndParameters); + QueryFragmentsAndParameters queryFragmentsAndParameters); /** * An interface for controlling query execution. @@ -323,20 +318,26 @@ public interface Neo4jOperations { interface ExecutableQuery { /** - * @return The list of all results. That can be an empty list but is never null. + * The list of all results. That can be an empty list but is never null. + * @return the list of all results */ List getResults(); /** - * @return An optional, single result. - * @throws IncorrectResultSizeDataAccessException when there is more than one result + * Returns an optional, single result. + * @return an optional, single result + * @throws IncorrectResultSizeDataAccessException when there is more than one + * result */ Optional getSingleResult(); /** - * @return A required, single result. + * Returns A required, single result. + * @return a required, single result * @throws NoResultException when there is no result */ T getRequiredSingleResult(); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java index 18923b9f8..7554b3014 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslator.java @@ -37,6 +37,7 @@ import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.exceptions.TransactionNestingException; import org.neo4j.driver.exceptions.TransientException; import org.neo4j.driver.exceptions.value.ValueException; + import org.springframework.core.log.LogAccessor; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; @@ -48,67 +49,20 @@ import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.dao.support.PersistenceExceptionTranslator; /** - * A PersistenceExceptionTranslator to get picked up by the Spring exception translation infrastructure. + * A PersistenceExceptionTranslator to get picked up by the Spring exception translation + * infrastructure. * * @author Michael J. Simons - * @soundtrack Kummer - KIOX * @since 6.0.3 */ @API(status = API.Status.STABLE, since = "6.0.3") public final class Neo4jPersistenceExceptionTranslator implements PersistenceExceptionTranslator { - private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jPersistenceExceptionTranslator.class)); + private static final LogAccessor log = new LogAccessor( + LogFactory.getLog(Neo4jPersistenceExceptionTranslator.class)); private static final Map>> ERROR_CODE_MAPPINGS; - @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - - if (ex instanceof DataAccessException) { - return (DataAccessException) ex; - } else if (ex instanceof DiscoveryException) { - return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); - } else if (ex instanceof DatabaseException) { - return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); - } else if (ex instanceof ServiceUnavailableException) { - return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); - } else if (ex instanceof SessionExpiredException) { - return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); - } else if (ex instanceof ProtocolException) { - return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); - } else if (ex instanceof TransientException) { - return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); - } else if (ex instanceof ValueException) { - return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); - } else if (ex instanceof AuthenticationException) { - return translateImpl((Neo4jException) ex, PermissionDeniedDataAccessException::new); - } else if (ex instanceof ResultConsumedException) { - return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); - } else if (ex instanceof FatalDiscoveryException) { - return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); - } else if (ex instanceof TransactionNestingException) { - return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); - } else if (ex instanceof ClientException) { - return translateImpl((Neo4jException) ex, InvalidDataAccessResourceUsageException::new); - } else if (ex instanceof Neo4jClient.IllegalDatabaseNameException) { - return null; - } - - log.warn(() -> String.format("Don't know how to translate exception of type %s", ex.getClass())); - return null; - } - - private static DataAccessException translateImpl(Neo4jException e, - BiFunction defaultTranslationProvider) { - - Optional optionalErrorCode = Optional.ofNullable(e.code()); - String msg = String.format("%s; Error code '%s'", e.getMessage(), optionalErrorCode.orElse("n/a")); - - return optionalErrorCode.flatMap(code -> ERROR_CODE_MAPPINGS.getOrDefault(code, Optional.empty())) - .orElse(defaultTranslationProvider).apply(msg, e); - } - static { Map>> tmp = new HashMap<>(); @@ -232,4 +186,66 @@ public final class Neo4jPersistenceExceptionTranslator implements PersistenceExc ERROR_CODE_MAPPINGS = Collections.unmodifiableMap(tmp); } + + private static DataAccessException translateImpl(Neo4jException e, + BiFunction defaultTranslationProvider) { + + Optional optionalErrorCode = Optional.ofNullable(e.code()); + String msg = String.format("%s; Error code '%s'", e.getMessage(), optionalErrorCode.orElse("n/a")); + + return optionalErrorCode.flatMap(code -> ERROR_CODE_MAPPINGS.getOrDefault(code, Optional.empty())) + .orElse(defaultTranslationProvider) + .apply(msg, e); + } + + @Override + @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + + if (ex instanceof DataAccessException) { + return (DataAccessException) ex; + } + else if (ex instanceof DiscoveryException) { + return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); + } + else if (ex instanceof DatabaseException) { + return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); + } + else if (ex instanceof ServiceUnavailableException) { + return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); + } + else if (ex instanceof SessionExpiredException) { + return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); + } + else if (ex instanceof ProtocolException) { + return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); + } + else if (ex instanceof TransientException) { + return translateImpl((Neo4jException) ex, TransientDataAccessResourceException::new); + } + else if (ex instanceof ValueException) { + return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); + } + else if (ex instanceof AuthenticationException) { + return translateImpl((Neo4jException) ex, PermissionDeniedDataAccessException::new); + } + else if (ex instanceof ResultConsumedException) { + return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); + } + else if (ex instanceof FatalDiscoveryException) { + return translateImpl((Neo4jException) ex, NonTransientDataAccessResourceException::new); + } + else if (ex instanceof TransactionNestingException) { + return translateImpl((Neo4jException) ex, InvalidDataAccessApiUsageException::new); + } + else if (ex instanceof ClientException) { + return translateImpl((Neo4jException) ex, InvalidDataAccessResourceUsageException::new); + } + else if (ex instanceof Neo4jClient.IllegalDatabaseNameException) { + return null; + } + + log.warn(() -> String.format("Don't know how to translate exception of type %s", ex.getClass())); + return null; + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java index 3129930a0..cd5fa7e47 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java @@ -18,34 +18,37 @@ package org.springframework.data.neo4j.core; import org.springframework.data.domain.ExampleMatcher; /** - * Contains some useful transformers for adding additional, supported transformations to {@link ExampleMatcher example matchers} via + * Contains some useful transformers for adding additional, supported transformations to + * {@link ExampleMatcher example matchers} via * {@link org.springframework.data.domain.ExampleMatcher#withTransformer(String, ExampleMatcher.PropertyValueTransformer)}. * * @author Michael J. Simons * @since 6.3.11 - * @soundtrack Subway To Sally - Herzblut */ public abstract class Neo4jPropertyValueTransformers { + private Neo4jPropertyValueTransformers() { + } + /** - * A transformer that will indicate that the generated condition for the specific property shall be negated, creating - * a {@code n.property != $property} for the equality operator for example. - * - * @return A value transformer negating values. + * A transformer that will indicate that the generated condition for the specific + * property shall be negated, creating a {@code n.property != $property} for the + * equality operator for example. + * @return a value transformer negating values. */ public static ExampleMatcher.PropertyValueTransformer notMatching() { return o -> o.map(NegatedValue::new); } /** - * A wrapper indicating a negated value (will be used as {@code n.property != $parameter} (in case of string properties - * all operators and not only the equality operator are supported, such as {@code not (n.property contains 'x')}. + * A wrapper indicating a negated value; will be used as + * {@code n.property != $parameter} (in case of string properties all operators and + * not only the equality operator are supported, such as + * {@code not (n.property contains 'x')}. * * @param value The value used in the negated condition. */ public record NegatedValue(Object value) { } - private Neo4jPropertyValueTransformers() { - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 5b18b7fb4..42d6f5d3c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.neo4j.cypherdsl.core.Cypher.anyNode; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; @@ -56,6 +52,7 @@ import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -105,19 +102,25 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; +import static org.neo4j.cypherdsl.core.Cypher.anyNode; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import static org.neo4j.cypherdsl.core.Cypher.parameter; + /** + * The Neo4j template combines various operations. All simple repositories will delegate + * to it. It provides a convenient way of dealing with mapped domain objects without + * having to define repositories for each type. + * * @author Michael J. Simons * @author Philipp Tölle * @author Gerrit Meier * @author Corey Beres - * @soundtrack Motörhead - We Are Motörhead * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @SuppressWarnings("DataFlowIssue") -public final class Neo4jTemplate implements - Neo4jOperations, FluentNeo4jOperations, - BeanClassLoaderAware, BeanFactoryAware { +public final class Neo4jTemplate + implements Neo4jOperations, FluentNeo4jOperations, BeanClassLoaderAware, BeanFactoryAware { private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jTemplate.class)); @@ -162,17 +165,18 @@ public final class Neo4jTemplate implements this(neo4jClient, neo4jMappingContext, EntityCallbacks.create()); } - public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, PlatformTransactionManager transactionManager) { + public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, + PlatformTransactionManager transactionManager) { this(neo4jClient, neo4jMappingContext, EntityCallbacks.create(), transactionManager); } public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, - EntityCallbacks entityCallbacks) { + EntityCallbacks entityCallbacks) { this(neo4jClient, neo4jMappingContext, entityCallbacks, null); } public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, - EntityCallbacks entityCallbacks, @Nullable PlatformTransactionManager platformTransactionManager) { + EntityCallbacks entityCallbacks, @Nullable PlatformTransactionManager platformTransactionManager) { Assert.notNull(neo4jClient, "The Neo4jClient is required"); Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required"); @@ -187,10 +191,11 @@ public final class Neo4jTemplate implements } ProjectionFactory getProjectionFactory() { - return Objects.requireNonNull(this.projectionFactory, "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean."); + return Objects.requireNonNull(this.projectionFactory, + "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean."); } - private T execute(TransactionCallback action) throws TransactionException { + private T execute(TransactionCallback action) throws TransactionException { return Objects.requireNonNull(Objects.requireNonNull(this.transactionTemplate).execute(action)); } @@ -205,9 +210,10 @@ public final class Neo4jTemplate implements @Override public long count(Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Cypher.count(asterisk())) - .build(); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + Statement statement = this.cypherGenerator.prepareMatchOf(entityMetaData) + .returning(Cypher.count(asterisk())) + .build(); return count(statement); } @@ -220,7 +226,7 @@ public final class Neo4jTemplate implements @Override public long count(Statement statement, Map parameters) { - return count(renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); + return count(this.renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); } @Override @@ -231,8 +237,10 @@ public final class Neo4jTemplate implements @Override public long count(String cypherQuery, Map parameters) { return executeReadOnly(tx -> { - PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery) - .withParameters(parameters).build(); + PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) + .withCypherQuery(cypherQuery) + .withParameters(parameters) + .build(); return toExecutableQuery(preparedQuery, true).getRequiredSingleResult(); }); } @@ -245,11 +253,11 @@ public final class Neo4jTemplate implements private List doFindAll(Class domainType, @Nullable Class resultType) { return executeReadOnly(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - return createExecutableQuery( - domainType, resultType, QueryFragmentsAndParameters.forFindAll(entityMetaData), true) - .getResults(); - }); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + return createExecutableQuery(domainType, resultType, QueryFragmentsAndParameters.forFindAll(entityMetaData), + true) + .getResults(); + }); } @Override @@ -264,7 +272,8 @@ public final class Neo4jTemplate implements @Override public Optional findOne(Statement statement, Map parameters, Class domainType) { - return executeReadOnly(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult()); + return executeReadOnly( + tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult()); } @Override @@ -274,12 +283,14 @@ public final class Neo4jTemplate implements @Override public List findAll(String cypherQuery, Map parameters, Class domainType) { - return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults()); + return executeReadOnly( + tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults()); } @Override public Optional findOne(String cypherQuery, Map parameters, Class domainType) { - return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult()); + return executeReadOnly( + tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult()); } @Override @@ -288,25 +299,31 @@ public final class Neo4jTemplate implements } @SuppressWarnings("unchecked") - List doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { + List doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, + Class resultType, TemplateSupport.FetchType fetchType, + @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { return executeReadOnly(tx -> { List intermediaResults; - if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) { + if (cypherQuery == null && queryFragmentsAndParameters == null + && fetchType == TemplateSupport.FetchType.ALL) { intermediaResults = doFindAll(domainType, resultType); - } else { + } + else { ExecutableQuery executableQuery; if (queryFragmentsAndParameters == null && cypherQuery != null) { executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, - parameters == null ? Collections.emptyMap() : parameters, - true); - } else { - executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(queryFragmentsAndParameters), true); + (parameters != null) ? parameters : Collections.emptyMap(), true); + } + else { + executableQuery = createExecutableQuery(domainType, resultType, + Objects.requireNonNull(queryFragmentsAndParameters), true); } intermediaResults = switch (fetchType) { case ALL -> executableQuery.getResults(); - case ONE -> executableQuery.getSingleResult().map(Collections::singletonList) - .orElseGet(Collections::emptyList); + case ONE -> executableQuery.getSingleResult() + .map(Collections::singletonList) + .orElseGet(Collections::emptyList); }; } @@ -316,27 +333,27 @@ public final class Neo4jTemplate implements if (resultType.isInterface()) { return intermediaResults.stream() - .map(instance -> getProjectionFactory().createProjection(resultType, instance)) - .collect(Collectors.toList()); + .map(instance -> getProjectionFactory().createProjection(resultType, instance)) + .collect(Collectors.toList()); } - DtoInstantiatingConverter converter = new DtoInstantiatingConverter(resultType, neo4jMappingContext); + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(resultType, this.neo4jMappingContext); return intermediaResults.stream() - .map(EntityInstanceWithSource.class::cast) - .map(converter::convert) - .map(v -> (R) v) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .map(EntityInstanceWithSource.class::cast) + .map(converter::convert) + .map(v -> (R) v) + .filter(Objects::nonNull) + .collect(Collectors.toList()); }); } @Override public boolean existsById(Object id, Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); - QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters - .forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); + QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters.forExistsById(entityMetaData, + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); Statement statement = fragmentsAndParameters.getQueryFragments().toStatement(); Map parameters = fragmentsAndParameters.getParameters(); @@ -347,27 +364,27 @@ public final class Neo4jTemplate implements @Override public Optional findById(Object id, Class domainType) { return executeReadOnly(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); - return createExecutableQuery(domainType, null, - QueryFragmentsAndParameters.forFindById(entityMetaData, - TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)), - true) - .getSingleResult(); - }); + return createExecutableQuery(domainType, null, + QueryFragmentsAndParameters.forFindById(entityMetaData, TemplateSupport + .convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)), + true) + .getSingleResult(); + }); } @Override public List findAllById(Iterable ids, Class domainType) { return executeReadOnly(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); - return createExecutableQuery(domainType, null, - QueryFragmentsAndParameters.forFindByAllId( - entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)), - true) - .getResults(); - }); + return createExecutableQuery(domainType, null, + QueryFragmentsAndParameters.forFindByAllId(entityMetaData, TemplateSupport + .convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)), + true) + .getResults(); + }); } @Override @@ -378,18 +395,17 @@ public final class Neo4jTemplate implements } @Override - @Nullable - public T saveAs(T instance, BiPredicate includeProperty) { + @Nullable public T saveAs(T instance, BiPredicate includeProperty) { if (instance == null) { return null; } - return execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); + return execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate( + this.neo4jMappingContext, instance.getClass(), includeProperty), null)); } @Override - @Nullable - public R saveAs(T instance, Class resultType) { + @Nullable public R saveAs(T instance, Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); if (instance == null) { @@ -398,56 +414,62 @@ public final class Neo4jTemplate implements return execute(tx -> { - if (resultType.equals(instance.getClass())) { - return resultType.cast(save(instance)); - } + if (resultType.equals(instance.getClass())) { + return resultType.cast(save(instance)); + } - ProjectionFactory localProjectionFactory = getProjectionFactory(); - ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); - Collection pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType, - localProjectionFactory, neo4jMappingContext); + ProjectionFactory localProjectionFactory = getProjectionFactory(); + ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); + Collection pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), + resultType, localProjectionFactory, this.neo4jMappingContext); - T savedInstance = saveImpl(instance, pps, null); - if (!resultType.isInterface()) { - @SuppressWarnings("unchecked") R result = (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedInstance); - return result; - } - if (projectionInformation.isClosed()) { - return localProjectionFactory.createProjection(resultType, savedInstance); - } + T savedInstance = saveImpl(instance, pps, null); + if (!resultType.isInterface()) { + @SuppressWarnings("unchecked") + R result = (R) new DtoInstantiatingConverter(resultType, this.neo4jMappingContext) + .convertDirectly(savedInstance); + return result; + } + if (projectionInformation.isClosed()) { + return localProjectionFactory.createProjection(resultType, savedInstance); + } - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass()); - Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return localProjectionFactory.createProjection(resultType, - this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()).orElseThrow()); - }); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(savedInstance.getClass()); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); + return localProjectionFactory.createProjection(resultType, this + .findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()) + .orElseThrow()); + }); } - private T saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { + private T saveImpl(T instance, Collection includedProperties, + @Nullable NestedRelationshipProcessingStateMachine stateMachine) { if (stateMachine != null && stateMachine.hasProcessedValue(instance)) { return instance; } - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(instance.getClass()); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(instance.getClass()); boolean isEntityNew = entityMetaData.isNew(instance); - T entityToBeSaved = eventSupport.maybeCallBeforeBind(instance); + T entityToBeSaved = this.eventSupport.maybeCallBeforeBind(instance); DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData); @SuppressWarnings("unchecked") // Applies to retrieving the meta data TemplateSupport.FilteredBinderFunction binderFunction = TemplateSupport.createAndApplyPropertyFilter( includedProperties, entityMetaData, - neo4jMappingContext.getRequiredBinderFunctionFor((Class) entityToBeSaved.getClass()) - ); - Optional newOrUpdatedNode = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) - .bind(entityToBeSaved) - .with(binderFunction) - .fetchAs(Entity.class) - .one(); + this.neo4jMappingContext.getRequiredBinderFunctionFor((Class) entityToBeSaved.getClass())); + Optional newOrUpdatedNode = this.neo4jClient + .query(() -> this.renderer.render(this.cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, + TemplateSupport.rendererRendersElementId(this.renderer)))) + .bind(entityToBeSaved) + .with(binderFunction) + .fetchAs(Entity.class) + .one(); if (newOrUpdatedNode.isEmpty()) { if (entityMetaData.hasVersionProperty()) { @@ -458,7 +480,8 @@ public final class Neo4jTemplate implements } Object elementId = newOrUpdatedNode.map(node -> { - if (!entityMetaData.isUsingDeprecatedInternalId() && TemplateSupport.rendererRendersElementId(renderer)) { + if (!entityMetaData.isUsingDeprecatedInternalId() + && TemplateSupport.rendererRendersElementId(this.renderer)) { return IdentitySupport.getElementId(node); } @SuppressWarnings("deprecation") @@ -471,7 +494,7 @@ public final class Neo4jTemplate implements TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode.get()); if (stateMachine == null) { - stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, instance, elementId); + stateMachine = new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext, instance, elementId); } stateMachine.markEntityAsProcessed(instance, elementId); @@ -488,21 +511,26 @@ public final class Neo4jTemplate implements PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - Neo4jClient.RunnableSpec runnableQuery = neo4jClient - .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty))) - .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()) - .to(Constants.NAME_OF_STATIC_LABELS_PARAM); + Neo4jClient.RunnableSpec runnableQuery = this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, + propertyAccessor.getProperty(idProperty))) + .to(Constants.NAME_OF_ID) + .bind(entityMetaData.getStaticLabels()) + .to(Constants.NAME_OF_STATIC_LABELS_PARAM); if (entityMetaData.hasVersionProperty()) { runnableQuery = runnableQuery - .bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty())) - .to(Constants.NAME_OF_VERSION_PARAM); + .bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty())) + .to(Constants.NAME_OF_VERSION_PARAM); } Optional> optionalResult = runnableQuery.fetch().one(); - return new DynamicLabels(entityMetaData, optionalResult.map(r -> (Collection) r.get(Constants.NAME_OF_LABELS)) - .orElseGet(Collections::emptyList), (Collection) propertyAccessor.getProperty(p)); + return new DynamicLabels(entityMetaData, + optionalResult.map(r -> (Collection) r.get(Constants.NAME_OF_LABELS)) + .orElseGet(Collections::emptyList), + (Collection) propertyAccessor.getProperty(p)); }).orElse(DynamicLabels.EMPTY); } @@ -512,13 +540,13 @@ public final class Neo4jTemplate implements } private boolean requiresSingleStatements(boolean heterogeneousCollection, Neo4jPersistentEntity entityMetaData) { - return heterogeneousCollection - || entityMetaData.isUsingInternalIds() - || entityMetaData.hasVersionProperty() + return heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty() || entityMetaData.getDynamicLabelsProperty().isPresent(); } - private List saveAllImpl(Iterable instances, @Nullable Collection includedProperties, @Nullable BiPredicate includeProperty) { + private List saveAllImpl(Iterable instances, + @Nullable Collection includedProperties, + @Nullable BiPredicate includeProperty) { Set> types = new HashSet<>(); List entities = new ArrayList<>(); @@ -534,23 +562,26 @@ public final class Neo4jTemplate implements boolean heterogeneousCollection = types.size() > 1; Class domainClass = types.iterator().next(); - Collection pps = includeProperty == null ? - Objects.requireNonNullElseGet(includedProperties, List::of) : - TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, - includeProperty); + Collection pps = (includeProperty != null) ? TemplateSupport + .computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, includeProperty) + : Objects.requireNonNullElseGet(includedProperties, List::of); - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainClass); if (requiresSingleStatements(heterogeneousCollection, entityMetaData)) { log.debug("Saving entities using single statements."); - NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); + NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine( + this.neo4jMappingContext); return entities.stream().map(e -> saveImpl(e, pps, stateMachine)).collect(Collectors.toList()); } class Tuple3 { + final T originalInstance; + final boolean wasNew; + final T modifiedInstance; Tuple3(T originalInstance, boolean wasNew, T modifiedInstance) { @@ -558,41 +589,52 @@ public final class Neo4jTemplate implements this.wasNew = wasNew; this.modifiedInstance = modifiedInstance; } + } List> entitiesToBeSaved = entities.stream() - .map(e -> new Tuple3<>(e, entityMetaData.isNew(e), eventSupport.maybeCallBeforeBind(e))) - .collect(Collectors.toList()); + .map(e -> new Tuple3<>(e, entityMetaData.isNew(e), this.eventSupport.maybeCallBeforeBind(e))) + .collect(Collectors.toList()); // Save roots - @SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it - Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor((Class) domainClass); + @SuppressWarnings("unchecked") // We can safely assume here that we have a + // humongous collection with only one single type + // being either T or extending it + Function> binderFunction = this.neo4jMappingContext + .getRequiredBinderFunctionFor((Class) domainClass); binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, entityMetaData, binderFunction); - List> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction) - .collect(Collectors.toList()); - Map idToInternalIdMapping = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) - .bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM) - .fetchAs(Map.Entry.class) - .mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) - .all() - .stream() - .collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (String) m.getValue())); + List> entityList = entitiesToBeSaved.stream() + .map(h -> h.modifiedInstance) + .map(binderFunction) + .collect(Collectors.toList()); + Map idToInternalIdMapping = this.neo4jClient + .query(() -> this.renderer.render(this.cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(entityList) + .to(Constants.NAME_OF_ENTITY_LIST_PARAM) + .fetchAs(Map.Entry.class) + .mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), + TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) + .all() + .stream() + .collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (String) m.getValue())); // Save related - var stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, null, null); + var stateMachine = new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext, null, null); return entitiesToBeSaved.stream().map(t -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - Object id = TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty)); + Object id = TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, + propertyAccessor.getProperty(idProperty)); String internalId = Objects.requireNonNull(idToInternalIdMapping.get(id)); stateMachine.registerInitialObject(t.originalInstance, internalId); - return this.processRelations(entityMetaData, propertyAccessor, t.wasNew, stateMachine, TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); + return this.processRelations(entityMetaData, propertyAccessor, t.wasNew, stateMachine, + TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); }).collect(Collectors.toList()); } @Override - public List saveAllAs(Iterable instances, BiPredicate includeProperty) { + public List saveAllAs(Iterable instances, + BiPredicate includeProperty) { return execute(tx -> saveAllImpl(instances, null, includeProperty)); } @@ -604,88 +646,103 @@ public final class Neo4jTemplate implements return execute(tx -> { - Class commonElementType = TemplateSupport.findCommonElementType(instances); + Class commonElementType = TemplateSupport.findCommonElementType(instances); - if (commonElementType == null) { - throw new IllegalArgumentException("Could not determine a common element of an heterogeneous collection"); - } + if (commonElementType == null) { + throw new IllegalArgumentException( + "Could not determine a common element of an heterogeneous collection"); + } - if (commonElementType == TemplateSupport.EmptyIterable.class) { - return Collections.emptyList(); - } + if (commonElementType == TemplateSupport.EmptyIterable.class) { + return Collections.emptyList(); + } - if (resultType.isAssignableFrom(commonElementType)) { - @SuppressWarnings("unchecked") // Nicer to live with this than streaming, mapping and collecting to avoid the cast. It's easier on the reactive side. - List saveElements = (List) saveAll(instances); - return saveElements; - } + if (resultType.isAssignableFrom(commonElementType)) { + @SuppressWarnings("unchecked") // Nicer to live with this than streaming, + // mapping and collecting to avoid the + // cast. It's easier on the reactive side. + List saveElements = (List) saveAll(instances); + return saveElements; + } - ProjectionFactory localProjectionFactory = getProjectionFactory(); - ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); + ProjectionFactory localProjectionFactory = getProjectionFactory(); + ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); - Collection pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType, - localProjectionFactory, neo4jMappingContext); + Collection pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, + resultType, localProjectionFactory, this.neo4jMappingContext); - List savedInstances = saveAllImpl(instances, pps, null); + List savedInstances = saveAllImpl(instances, pps, null); - if (projectionInformation.isClosed()) { - return savedInstances.stream().map(instance -> localProjectionFactory.createProjection(resultType, instance)) - .collect(Collectors.toList()); - } + if (projectionInformation.isClosed()) { + return savedInstances.stream() + .map(instance -> localProjectionFactory.createProjection(resultType, instance)) + .collect(Collectors.toList()); + } - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType); - Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(commonElementType); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - List ids = savedInstances.stream().map(savedInstance -> { - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return propertyAccessor.getProperty(idProperty); - }).collect(Collectors.toList()); + List ids = savedInstances.stream().map(savedInstance -> { + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); + return propertyAccessor.getProperty(idProperty); + }).collect(Collectors.toList()); - return findAllById(ids, commonElementType) - .stream().map(instance -> localProjectionFactory.createProjection(resultType, instance)) - .collect(Collectors.toList()); - }); + return findAllById(ids, commonElementType).stream() + .map(instance -> localProjectionFactory.createProjection(resultType, instance)) + .collect(Collectors.toList()); + }); } @Override public void deleteById(Object id, Class domainType) { - executeWithoutResult(tx -> { + executeWithoutResult(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - String nameOfParameter = "id"; - Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + String nameOfParameter = "id"; + Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)); - log.debug(() -> String.format("Deleting entity with id %s ", id)); + log.debug(() -> String.format("Deleting entity with id %s ", id)); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)) - .to(nameOfParameter).run(); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData, condition); + ResultSummary summary = this.neo4jClient.query(this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), + id)) + .to(nameOfParameter) + .run(); - log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), - summary.counters().relationshipsDeleted())); - }); + log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted())); + }); } @Override public void deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, - @Nullable Object versionValue) { + @Nullable Object versionValue) { executeWithoutResult(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); String nameOfParameter = "id"; - Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)) - .and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()) - .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) - .or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull())); + Condition condition = entityMetaData.getIdExpression() + .isEqualTo(parameter(nameOfParameter)) + .and(Cypher + .property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), + versionProperty.getPropertyName()) + .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) + .or(Cypher + .property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), + versionProperty.getPropertyName()) + .isNull())); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition) - .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build(); + Statement statement = this.cypherGenerator.prepareMatchOf(entityMetaData, condition) + .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)) + .build(); Map parameters = new HashMap<>(); - parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); + parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, + entityMetaData.getRequiredIdProperty(), id)); parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue); var lockedEntity = createExecutableQuery(domainType, null, statement, parameters, false).getSingleResult(); @@ -700,38 +757,41 @@ public final class Neo4jTemplate implements @Override public void deleteAllById(Iterable ids, Class domainType) { - executeWithoutResult(tx -> { + executeWithoutResult(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - String nameOfParameter = "ids"; - Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter)); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + String nameOfParameter = "ids"; + Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter)); - log.debug(() -> String.format("Deleting all entities with the following ids: %s ", ids)); + log.debug(() -> String.format("Deleting all entities with the following ids: %s ", ids)); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)) - .to(nameOfParameter).run(); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData, condition); + ResultSummary summary = this.neo4jClient.query(this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), + ids)) + .to(nameOfParameter) + .run(); - log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), - summary.counters().relationshipsDeleted())); - }); + log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted())); + }); } @Override public void deleteAll(Class domainType) { - executeWithoutResult(tx -> { + executeWithoutResult(tx -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - log.debug(() -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel())); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + log.debug( + () -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel())); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData); - ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).run(); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData); + ResultSummary summary = this.neo4jClient.query(this.renderer.render(statement)).run(); - log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), - summary.counters().relationshipsDeleted())); - }); + log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted())); + }); } private ExecutableQuery createExecutableQuery(Class domainType, Statement statement, boolean readOnly) { @@ -742,69 +802,62 @@ public final class Neo4jTemplate implements return createExecutableQuery(domainType, null, cypherQuery, Collections.emptyMap(), readOnly); } - private ExecutableQuery createExecutableQuery(Class domainType, @Nullable Class resultType, Statement statement, Map parameters, boolean readOnly) { + private ExecutableQuery createExecutableQuery(Class domainType, @Nullable Class resultType, + Statement statement, Map parameters, boolean readOnly) { - return createExecutableQuery(domainType, resultType, renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters), readOnly); + return createExecutableQuery(domainType, resultType, this.renderer.render(statement), + TemplateSupport.mergeParameters(statement, parameters), readOnly); } - private ExecutableQuery createExecutableQuery( - Class domainType, - @Nullable Class resultType, - String cypherStatement, - Map parameters, - boolean readOnly) { + private ExecutableQuery createExecutableQuery(Class domainType, @Nullable Class resultType, + String cypherStatement, Map parameters, boolean readOnly) { Supplier> mappingFunction = TemplateSupport - .getAndDecorateMappingFunction(neo4jMappingContext, domainType, resultType); + .getAndDecorateMappingFunction(this.neo4jMappingContext, domainType, resultType); PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType) - .withCypherQuery(cypherStatement) - .withParameters(parameters) - .usingMappingFunction(mappingFunction) - .build(); + .withCypherQuery(cypherStatement) + .withParameters(parameters) + .usingMappingFunction(mappingFunction) + .build(); return toExecutableQuery(preparedQuery, readOnly); } /** * Starts of processing of the relationships. - * - * @param neo4jPersistentEntity The description of the instance to save - * @param parentPropertyAccessor The property accessor of the parent, to modify the relationships - * @param isParentObjectNew A flag if the parent was new - * @param stateMachine Initial state of entity processing - * @param includeProperty A predicate telling to include a relationship property or not - * @param The type of the object being initially processed - * @return The owner of the relations being processed + * @param neo4jPersistentEntity the description of the instance to save + * @param parentPropertyAccessor the property accessor of the parent, to modify the + * relationships + * @param isParentObjectNew a flag if the parent was new + * @param stateMachine initial state of entity processing + * @param includeProperty a predicate telling to include a relationship property or + * not + * @param the type of the object being initially processed + * @return the owner of the relations being processed */ - private T processRelations( - Neo4jPersistentEntity neo4jPersistentEntity, - PersistentPropertyAccessor parentPropertyAccessor, - boolean isParentObjectNew, - NestedRelationshipProcessingStateMachine stateMachine, - PropertyFilter includeProperty - ) { + private T processRelations(Neo4jPersistentEntity neo4jPersistentEntity, + PersistentPropertyAccessor parentPropertyAccessor, boolean isParentObjectNew, + NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty) { - PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass()); - return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, - stateMachine, includeProperty, startingPropertyPath); + PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath + .withRootType(neo4jPersistentEntity.getUnderlyingClass()); + return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, stateMachine, + includeProperty, startingPropertyPath); } @SuppressWarnings("deprecation") - private T processNestedRelations( - Neo4jPersistentEntity sourceEntity, - PersistentPropertyAccessor propertyAccessor, - boolean isParentObjectNew, - NestedRelationshipProcessingStateMachine stateMachine, - PropertyFilter includeProperty, - PropertyFilter.RelaxedPropertyPath previousPath - ) { + private T processNestedRelations(Neo4jPersistentEntity sourceEntity, + PersistentPropertyAccessor propertyAccessor, boolean isParentObjectNew, + NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, + PropertyFilter.RelaxedPropertyPath previousPath) { Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty()); AssociationHandlerSupport.of(sourceEntity).doWithAssociations(association -> { // create context to bundle parameters - NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor, sourceEntity); + NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor, + sourceEntity); if (relationshipContext.isReadOnly()) { return; } @@ -815,7 +868,8 @@ public final class Neo4jTemplate implements RelationshipDescription relationshipDescription = relationshipContext.getRelationship(); - PropertyFilter.RelaxedPropertyPath currentPropertyPath = previousPath.append(relationshipDescription.getFieldName()); + PropertyFilter.RelaxedPropertyPath currentPropertyPath = previousPath + .append(relationshipDescription.getFieldName()); if (!includeProperty.isNotFiltering() && !includeProperty.contains(currentPropertyPath)) { return; @@ -823,21 +877,27 @@ public final class Neo4jTemplate implements Neo4jPersistentProperty idProperty; if (!relationshipDescription.hasInternalIdProperty()) { idProperty = null; - } else { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); - idProperty = (relationshipPropertiesEntity == null) ? null : relationshipPropertiesEntity.getIdProperty(); + } + else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription + .getRelationshipPropertiesEntity(); + idProperty = (relationshipPropertiesEntity != null) ? relationshipPropertiesEntity.getIdProperty() + : null; } // break recursive procession and deletion of previously created relationships ProcessState processState = stateMachine.getStateOf(fromId, relationshipDescription, relatedValuesToStore); - if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS || processState == ProcessState.PROCESSED_BOTH) { + if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS + || processState == ProcessState.PROCESSED_BOTH) { return; } - // Remove all relationships before creating all new if the entity is not new and the relationship + // Remove all relationships before creating all new if the entity is not new + // and the relationship // has not been processed before. - // This avoids the usage of cache but might have significant impact on overall performance - boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer); + // This avoids the usage of cache but might have significant impact on overall + // performance + boolean canUseElementId = TemplateSupport.rendererRendersElementId(this.renderer); if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) { List knownRelationshipsIds = new ArrayList<>(); @@ -847,7 +907,8 @@ public final class Neo4jTemplate implements continue; } - PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); if (relationshipPropertiesPropertyAccessor == null) { continue; } @@ -858,14 +919,16 @@ public final class Neo4jTemplate implements } } - Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription, canUseElementId); + Statement relationshipRemoveQuery = this.cypherGenerator.prepareDeleteOf(sourceEntity, + relationshipDescription, canUseElementId); - neo4jClient.query(renderer.render(relationshipRemoveQuery)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(knownRelationshipsIds) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // - .run(); + this.neo4jClient.query(this.renderer.render(relationshipRemoveQuery)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), + fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(knownRelationshipsIds) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // + .run(); } // nothing to do because there is nothing to map @@ -887,166 +950,209 @@ public final class Neo4jTemplate implements for (Object relatedValueToStore : relatedValuesToStore) { // here a map entry is not always anymore a dynamic association - Object relatedObjectBeforeCallbacksApplied = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore); - Neo4jPersistentEntity targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass()); + Object relatedObjectBeforeCallbacksApplied = relationshipContext + .identifyAndExtractRelationshipTargetNode(relatedValueToStore); + Neo4jPersistentEntity targetEntity = this.neo4jMappingContext + .getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass()); boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied); Object newRelatedObject = stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied) ? stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied) - : eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied); + : this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied); Object relatedInternalId; Entity savedEntity = null; // No need to save values if processed if (stateMachine.hasProcessedValue(relatedValueToStore)) { relatedInternalId = stateMachine.getObjectId(relatedValueToStore); - } else { + } + else { if (isNewEntity || relationshipDescription.cascadeUpdates()) { - savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath); - } else { + savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, + currentPropertyPath); + } + else { var targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); var requiredIdProperty = targetEntity.getRequiredIdProperty(); - savedEntity = loadRelatedNode(targetEntity, targetPropertyAccessor.getProperty(requiredIdProperty)); + savedEntity = loadRelatedNode(targetEntity, + targetPropertyAccessor.getProperty(requiredIdProperty)); } - relatedInternalId = TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? savedEntity.elementId() : savedEntity.id(); + relatedInternalId = TemplateSupport.rendererCanUseElementIdIfPresent(this.renderer, targetEntity) + ? savedEntity.elementId() : savedEntity.id(); stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId); if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { - Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity(); + Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore) + .getRelatedEntity(); stateMachine.markAsAliased(entity, relatedInternalId); } } Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty(); - PersistentPropertyAccessor targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); + PersistentPropertyAccessor targetPropertyAccessor = targetEntity + .getPropertyAccessor(newRelatedObject); Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty); - relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId); + relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, + Optional.ofNullable(savedEntity), relatedInternalId); if (savedEntity != null) { TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity); } stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean()); - stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, + stateMachine.markRelationshipAsProcessed( + (possibleInternalLongId != null) ? possibleInternalLongId : relatedInternalId, relationshipDescription.getRelationshipObverse()); Object idValue; PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); if (idProperty == null || relationshipPropertiesPropertyAccessor == null) { idValue = null; - } else { + } + else { idValue = relationshipPropertiesPropertyAccessor.getProperty(idProperty); } Map properties = new HashMap<>(); - properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); + properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport + .convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); boolean isNewRelationship = idValue == null; if (relationshipDescription.isDynamic()) { // create new dynamic relationship properties - if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) { - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship( - sourceEntity, relationshipDescription, relatedValueToStore, true, canUseElementId); + if (relationshipDescription.hasRelationshipProperties() && isNewRelationship + && idProperty != null) { + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForSingleRelationship(sourceEntity, relationshipDescription, + relatedValueToStore, true, canUseElementId); List row = Collections.singletonList(properties); statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row); - Optional relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(relatedInternalId) // - .to(Constants.TO_ID_PARAMETER_NAME) // - .bind(idValue) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // - .bindAll(statementHolder.getProperties()) - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .one(); + Optional relationshipInternalId = this.neo4jClient + .query(this.renderer.render(statementHolder.getStatement())) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, + sourceEntity.getRequiredIdProperty(), fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(relatedInternalId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .bind(idValue) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // + .bindAll(statementHolder.getProperties()) + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one(); - assignIdToRelationshipProperties(relationshipContext, relatedValueToStore, idProperty, relationshipInternalId.orElseThrow()); - } else { // plain (new or to update) dynamic relationship or dynamic relationships with properties to update - - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship( - sourceEntity, relationshipDescription, relatedValueToStore, false, canUseElementId); - - List row = Collections.singletonList(properties); - statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row); - neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(relatedInternalId) // - .to(Constants.TO_ID_PARAMETER_NAME) // - .bind(idValue) - .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // - .bindAll(statementHolder.getProperties()) - .run(); + assignIdToRelationshipProperties(relationshipContext, relatedValueToStore, idProperty, + relationshipInternalId.orElseThrow()); } - } else if (relationshipDescription.hasRelationshipProperties() && fromId != null) { + else { // plain (new or to update) dynamic relationship or dynamic + // relationships with properties to update + + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForSingleRelationship(sourceEntity, relationshipDescription, + relatedValueToStore, false, canUseElementId); + + List row = Collections.singletonList(properties); + statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row); + this.neo4jClient.query(this.renderer.render(statementHolder.getStatement())) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, + sourceEntity.getRequiredIdProperty(), fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(relatedInternalId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .bind(idValue) + .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // + .bindAll(statementHolder.getProperties()) + .run(); + } + } + else if (relationshipDescription.hasRelationshipProperties() && fromId != null) { // check if bidi mapped already var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); - var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity( + propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); if (hasProcessedRelationshipEntity) { - stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, relatedInternalId, relationshipContext, relatedValueToStore, idProperty); - } else { + stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, + relatedInternalId, relationshipContext, relatedValueToStore, idProperty); + } + else { if (isNewRelationship && idProperty != null) { newRelationshipPropertiesRows.add(properties); newRelationshipPropertiesToStore.add(relatedValueToStore); - } else { - neo4jMappingContext.getEntityConverter().write(hlp.getRelationshipProperties(), properties); + } + else { + this.neo4jMappingContext.getEntityConverter() + .write(hlp.getRelationshipProperties(), properties); relationshipPropertiesRows.add(properties); } - stateMachine.storeProcessRelationshipEntity(hlp, propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + stateMachine.storeProcessRelationshipEntity(hlp, propertyAccessor.getBean(), + hlp.getRelatedEntity(), relationshipContext.getRelationship()); } - } else { + } + else { // non-dynamic relationship or relationship with properties plainRelationshipRows.add(properties); } if (processState != ProcessState.PROCESSED_ALL_VALUES) { - processNestedRelations(targetEntity, targetPropertyAccessor, isNewEntity, stateMachine, includeProperty, currentPropertyPath); + processNestedRelations(targetEntity, targetPropertyAccessor, isNewEntity, stateMachine, + includeProperty, currentPropertyPath); } - Object potentiallyRecreatedNewRelatedObject = MappingSupport.getRelationshipOrRelationshipPropertiesObject(neo4jMappingContext, - relationshipDescription.hasRelationshipProperties(), - relationshipProperty.isDynamicAssociation(), - relatedValueToStore, - targetPropertyAccessor); - relationshipHandler.handle(relatedValueToStore, relatedObjectBeforeCallbacksApplied, potentiallyRecreatedNewRelatedObject); + Object potentiallyRecreatedNewRelatedObject = MappingSupport + .getRelationshipOrRelationshipPropertiesObject(this.neo4jMappingContext, + relationshipDescription.hasRelationshipProperties(), + relationshipProperty.isDynamicAssociation(), relatedValueToStore, targetPropertyAccessor); + relationshipHandler.handle(relatedValueToStore, relatedObjectBeforeCallbacksApplied, + potentiallyRecreatedNewRelatedObject); } // batch operations - if (!(relationshipDescription.hasRelationshipProperties() || relationshipDescription.isDynamic() || plainRelationshipRows.isEmpty())) { - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeSimpleRelationshipBatch( - sourceEntity, relationshipDescription, plainRelationshipRows, canUseElementId); - statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, plainRelationshipRows); - neo4jClient.query(renderer.render(statementHolder.getStatement())) + if (!(relationshipDescription.hasRelationshipProperties() || relationshipDescription.isDynamic() + || plainRelationshipRows.isEmpty())) { + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForImperativeSimpleRelationshipBatch(sourceEntity, relationshipDescription, + plainRelationshipRows, canUseElementId); + statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, + plainRelationshipRows); + this.neo4jClient.query(this.renderer.render(statementHolder.getStatement())) + .bindAll(statementHolder.getProperties()) + .run(); + } + else if (relationshipDescription.hasRelationshipProperties()) { + if (!relationshipPropertiesRows.isEmpty()) { + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForImperativeRelationshipsWithPropertiesBatch(false, sourceEntity, + relationshipDescription, updateRelatedValuesToStore, relationshipPropertiesRows, + canUseElementId); + statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, + relationshipPropertiesRows); + + this.neo4jClient.query(this.renderer.render(statementHolder.getStatement())) .bindAll(statementHolder.getProperties()) .run(); - } else if (relationshipDescription.hasRelationshipProperties()) { - if (!relationshipPropertiesRows.isEmpty()) { - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(false, - sourceEntity, relationshipDescription, updateRelatedValuesToStore, relationshipPropertiesRows, canUseElementId); - statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, relationshipPropertiesRows); - - neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bindAll(statementHolder.getProperties()) - .run(); } if (!(newRelationshipPropertiesToStore.isEmpty() || idProperty == null)) { - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true, - sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId); - List all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement())) - .bindAll(statementHolder.getProperties()) - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .all()); + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForImperativeRelationshipsWithPropertiesBatch(true, sourceEntity, + relationshipDescription, newRelationshipPropertiesToStore, + newRelationshipPropertiesRows, canUseElementId); + List all = new ArrayList<>( + this.neo4jClient.query(this.renderer.render(statementHolder.getStatement())) + .bindAll(statementHolder.getProperties()) + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .all()); // assign new ids for (int i = 0; i < all.size(); i++) { Object anId = all.get(i); - assignIdToRelationshipProperties(relationshipContext, newRelationshipPropertiesToStore.get(i), idProperty, anId); + assignIdToRelationshipProperties(relationshipContext, newRelationshipPropertiesToStore.get(i), + idProperty, anId); } } } - // Possible grab missing relationship ids now for bidirectional ones, with properties, mapped in opposite directions + // Possible grab missing relationship ids now for bidirectional ones, with + // properties, mapped in opposite directions stateMachine.updateRelationshipIds(this::getRelationshipId); relationshipHandler.applyFinalResultToOwner(propertyAccessor); @@ -1057,111 +1163,120 @@ public final class Neo4jTemplate implements return finalSubgraphRoot; } - private Optional getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + private Optional getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, + Object fromId, Object toId) { - return neo4jClient.query(renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(toId) // - .to(Constants.TO_ID_PARAMETER_NAME) // - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .one(); + return this.neo4jClient.query(this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(toId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one(); } - - // The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} + // The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, + // PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id private Entity loadRelatedNode(NodeDescription targetNodeDescription, @Nullable Object relatedInternalId) { var targetPersistentEntity = (Neo4jPersistentEntity) targetNodeDescription; - var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); + var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, + TemplateSupport.convertIdValues(this.neo4jMappingContext, + targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue(); - return neo4jClient - .query(() -> renderer.render( - cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), - queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build())) - .bindAll(queryFragmentsAndParameters.getParameters()) - .fetchAs(Entity.class).mappedBy((t, r) -> r.get(nodeName).asNode()) - .one().orElseThrow(); + return this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator + .prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), + queryFragmentsAndParameters.getQueryFragments().getCondition()) + .returning(nodeName) + .build())) + .bindAll(queryFragmentsAndParameters.getParameters()) + .fetchAs(Entity.class) + .mappedBy((t, r) -> r.get(nodeName).asNode()) + .one() + .orElseThrow(); } - private void assignIdToRelationshipProperties( - NestedRelationshipContext relationshipContext, - Object relatedValueToStore, - Neo4jPersistentProperty idProperty, - Object relationshipInternalId - ) { + private void assignIdToRelationshipProperties(NestedRelationshipContext relationshipContext, + Object relatedValueToStore, Neo4jPersistentProperty idProperty, Object relationshipInternalId) { PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); if (relationshipPropertiesPropertyAccessor != null) { - relationshipPropertiesPropertyAccessor - .setProperty(idProperty, relationshipInternalId); + relationshipPropertiesPropertyAccessor.setProperty(idProperty, relationshipInternalId); } } - private Entity saveRelatedNode(Object entity, NodeDescription targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) { + private Entity saveRelatedNode(Object entity, NodeDescription targetNodeDescription, + PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) { Neo4jPersistentEntity targetPersistentEntity = (Neo4jPersistentEntity) targetNodeDescription; DynamicLabels dynamicLabels = determineDynamicLabels(entity, targetPersistentEntity); @SuppressWarnings("rawtypes") Class entityType = targetPersistentEntity.getType(); @SuppressWarnings("unchecked") - Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(entityType); + Function> binderFunction = this.neo4jMappingContext + .getRequiredBinderFunctionFor(entityType); binderFunction = binderFunction.andThen(tree -> { @SuppressWarnings("unchecked") Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); String idPropertyName = targetPersistentEntity.getRequiredIdProperty().getPropertyName(); IdDescription idDescription = targetPersistentEntity.getIdDescription(); - boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); + boolean assignedId = idDescription != null + && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); if (properties != null && !includeProperty.isNotFiltering()) { - properties.entrySet() - .removeIf(e -> { - // we cannot skip the id property if it is an assigned id - boolean isIdProperty = e.getKey().equals(idPropertyName); - return !(assignedId && isIdProperty) && !includeProperty.contains(currentPropertyPath.append(e.getKey())); - }); + properties.entrySet().removeIf(e -> { + // we cannot skip the id property if it is an assigned id + boolean isIdProperty = e.getKey().equals(idPropertyName); + return !(assignedId && isIdProperty) + && !includeProperty.contains(currentPropertyPath.append(e.getKey())); + }); } return tree; }); - Optional optionalSavedNode = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) - .bind(entity).with(binderFunction) - .fetchAs(Entity.class) - .one(); + Optional optionalSavedNode = this.neo4jClient + .query(() -> this.renderer.render(this.cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, + TemplateSupport.rendererRendersElementId(this.renderer)))) + .bind(entity) + .with(binderFunction) + .fetchAs(Entity.class) + .one(); if (targetPersistentEntity.hasVersionProperty() && !optionalSavedNode.isPresent()) { throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE); } // It is checked above, god dammit. - //noinspection OptionalGetWithoutIsPresent + // noinspection OptionalGetWithoutIsPresent return optionalSavedNode.get(); } @Override public void setBeanClassLoader(ClassLoader beanClassLoader) { - //noinspection ConstantValue - this.beanClassLoader = beanClassLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : beanClassLoader; + // noinspection ConstantValue + this.beanClassLoader = (beanClassLoader != null) ? beanClassLoader + : org.springframework.util.ClassUtils.getDefaultClassLoader(); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.eventSupport = EventSupport.discoverCallbacks(neo4jMappingContext, beanFactory); + this.eventSupport = EventSupport.discoverCallbacks(this.neo4jMappingContext, beanFactory); SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader)); spelAwareProxyProjectionFactory.setBeanFactory(beanFactory); this.projectionFactory = spelAwareProxyProjectionFactory; - Configuration cypherDslConfiguration = beanFactory - .getBeanProvider(Configuration.class) - .getIfAvailable(Configuration::defaultConfig); + Configuration cypherDslConfiguration = beanFactory.getBeanProvider(Configuration.class) + .getIfAvailable(Configuration::defaultConfig); this.renderer = Renderer.getRenderer(cypherDslConfiguration); - this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect()); - this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction); + this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction + .apply(cypherDslConfiguration.getDialect()); + this.cypherGenerator.setElementIdOrIdFunction(this.elementIdOrIdFunction); if (this.transactionTemplate != null && this.transactionTemplateReadOnly != null) { return; @@ -1173,8 +1288,8 @@ public final class Neo4jTemplate implements PlatformTransactionManager transactionManagerCandidate = it.next(); if (transactionManagerCandidate instanceof Neo4jTransactionManager neo4jTransactionManager) { if (transactionManager != null) { - throw new IllegalStateException("Multiple Neo4jTransactionManagers are defined in this context. " + - "If this in intended, please pass the transaction manager to use with this Neo4jTemplate in the constructor"); + throw new IllegalStateException("Multiple Neo4jTransactionManagers are defined in this context. " + + "If this in intended, please pass the transaction manager to use with this Neo4jTemplate in the constructor"); } transactionManager = neo4jTransactionManager; } @@ -1197,30 +1312,26 @@ public final class Neo4jTemplate implements @Override public ExecutableQuery toExecutableQuery(Class domainType, - QueryFragmentsAndParameters queryFragmentsAndParameters) { + QueryFragmentsAndParameters queryFragmentsAndParameters) { return createExecutableQuery(domainType, null, queryFragmentsAndParameters, false); } - - private ExecutableQuery createExecutableQuery( - Class domainType, - @Nullable Class resultType, - QueryFragmentsAndParameters queryFragmentsAndParameters, - boolean readOnlyTransaction) { + private ExecutableQuery createExecutableQuery(Class domainType, @Nullable Class resultType, + QueryFragmentsAndParameters queryFragmentsAndParameters, boolean readOnlyTransaction) { Supplier> mappingFunction = TemplateSupport - .getAndDecorateMappingFunction(neo4jMappingContext, domainType, resultType); + .getAndDecorateMappingFunction(this.neo4jMappingContext, domainType, resultType); PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType) - .withQueryFragmentsAndParameters(queryFragmentsAndParameters) - .usingMappingFunction(mappingFunction) - .build(); + .withQueryFragmentsAndParameters(queryFragmentsAndParameters) + .usingMappingFunction(mappingFunction) + .build(); return toExecutableQuery(preparedQuery, readOnlyTransaction); } @Override public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { - return toExecutableQuery(preparedQuery, false); + return toExecutableQuery(preparedQuery, false); } private ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery, boolean readOnly) { @@ -1238,137 +1349,164 @@ public final class Neo4jTemplate implements return Collections.emptyList(); } - Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType)); + Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), + () -> "Could not find a common type element to store and then project multiple instances of type %s" + .formatted(domainType)); return execute(tx -> { - Collection pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType, - getProjectionFactory(), neo4jMappingContext); + Collection pps = PropertyFilterSupport.addPropertiesFrom(domainType, + resultType, getProjectionFactory(), this.neo4jMappingContext); - NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); - List results = new ArrayList<>(); - EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); - for (R instance : instances) { - T domainObject = converter.convert(instance); - if (domainObject == null) { - continue; - } - T savedEntity = saveImpl(domainObject, pps, stateMachine); + NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine( + this.neo4jMappingContext); + List results = new ArrayList<>(); + EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, + this.neo4jMappingContext); + for (R instance : instances) { + T domainObject = converter.convert(instance); + if (domainObject == null) { + continue; + } + T savedEntity = saveImpl(domainObject, pps, stateMachine); - @SuppressWarnings("unchecked") - R convertedBack = (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity); - results.add(convertedBack); - } - return results; - }); + @SuppressWarnings("unchecked") + R convertedBack = (R) new DtoInstantiatingConverter(resultType, this.neo4jMappingContext) + .convertDirectly(savedEntity); + results.add(convertedBack); + } + return results; + }); } String render(Statement statement) { - return renderer.render(statement); + return this.renderer.render(statement); } final class DefaultExecutableQuery implements ExecutableQuery { private final PreparedQuery preparedQuery; + private final TransactionTemplate txTemplate; DefaultExecutableQuery(PreparedQuery preparedQuery, boolean readOnly) { this.preparedQuery = preparedQuery; // At this time, both must be initialized - this.txTemplate = Objects.requireNonNull(readOnly ? transactionTemplateReadOnly : transactionTemplate); + this.txTemplate = Objects.requireNonNull( + readOnly ? Neo4jTemplate.this.transactionTemplateReadOnly : Neo4jTemplate.this.transactionTemplate); } - - @SuppressWarnings({"unchecked", "NullAway"}) + @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public List getResults() { - return txTemplate - .execute(tx -> { - Collection all = createFetchSpec().map(Neo4jClient.RecordFetchSpec::all).orElse(Collections.emptyList()); - if (preparedQuery.resultsHaveBeenAggregated()) { - return all.stream().flatMap(nested -> ((Collection) nested).stream()).distinct().collect(Collectors.toList()); - } - return new ArrayList<>(all); - }); + return this.txTemplate.execute(tx -> { + Collection all = createFetchSpec().map(Neo4jClient.RecordFetchSpec::all) + .orElse(Collections.emptyList()); + if (this.preparedQuery.resultsHaveBeenAggregated()) { + return all.stream() + .flatMap(nested -> ((Collection) nested).stream()) + .distinct() + .collect(Collectors.toList()); + } + return new ArrayList<>(all); + }); } - @SuppressWarnings({"unchecked", "NullAway"}) + @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public Optional getSingleResult() { - return txTemplate.execute(tx -> { + return this.txTemplate.execute(tx -> { try { Optional one = createFetchSpec().flatMap(Neo4jClient.RecordFetchSpec::one); - if (preparedQuery.resultsHaveBeenAggregated()) { + if (this.preparedQuery.resultsHaveBeenAggregated()) { return one.map(aggregatedResults -> ((LinkedHashSet) aggregatedResults).iterator().next()); } return one; - } catch (NoSuchRecordException e) { - // This exception is thrown by the driver in both cases when there are 0 or 1+n records - // So there has been an incorrect result size, but not too few results but too many. - throw new IncorrectResultSizeDataAccessException(e.getMessage(), 1); + } + catch (NoSuchRecordException ex) { + // This exception is thrown by the driver in both cases when there are + // 0 or 1+n records + // So there has been an incorrect result size, but not too few results + // but too many. + throw new IncorrectResultSizeDataAccessException(ex.getMessage(), 1); } }); } - @SuppressWarnings({"unchecked", "NullAway"}) + @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public T getRequiredSingleResult() { - return txTemplate.execute(tx -> { + return this.txTemplate.execute(tx -> { Optional one = createFetchSpec().flatMap(Neo4jClient.RecordFetchSpec::one); - if (preparedQuery.resultsHaveBeenAggregated()) { + if (this.preparedQuery.resultsHaveBeenAggregated()) { one = one.map(aggregatedResults -> ((LinkedHashSet) aggregatedResults).iterator().next()); } - return one.orElseThrow(() -> new NoResultException(1, preparedQuery.getQueryFragmentsAndParameters().getCypherQuery())); + return one.orElseThrow(() -> new NoResultException(1, + this.preparedQuery.getQueryFragmentsAndParameters().getCypherQuery())); }); } private Optional> createFetchSpec() { - QueryFragmentsAndParameters queryFragmentsAndParameters = preparedQuery.getQueryFragmentsAndParameters(); + QueryFragmentsAndParameters queryFragmentsAndParameters = this.preparedQuery + .getQueryFragmentsAndParameters(); String cypherQuery = queryFragmentsAndParameters.getCypherQuery(); Map finalParameters = queryFragmentsAndParameters.getParameters(); QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments(); - Neo4jPersistentEntity entityMetaData = (Neo4jPersistentEntity) queryFragmentsAndParameters.getNodeDescription(); + Neo4jPersistentEntity entityMetaData = (Neo4jPersistentEntity) queryFragmentsAndParameters + .getNodeDescription(); - boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField); + boolean containsPossibleCircles = entityMetaData != null + && entityMetaData.containsPossibleCircles(queryFragments::includeField); if (cypherQuery == null || containsPossibleCircles) { Statement statement; - // The null check for the metadata is superfluous, but the easiest way to make NullAway happy + // The null check for the metadata is superfluous, but the easiest way to + // make NullAway happy if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) { - NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById = - createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters()); + NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById = createNodesAndRelationshipsByIdStatementProvider( + entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters()); if (!nodesAndRelationshipsById.hasRootNodeIds()) { return Optional.empty(); } statement = nodesAndRelationshipsById.toStatement(entityMetaData); - } else { + } + else { statement = queryFragments.toStatement(); } - cypherQuery = renderer.render(statement); + cypherQuery = Neo4jTemplate.this.renderer.render(statement); finalParameters = TemplateSupport.mergeParameters(statement, finalParameters); } - Neo4jClient.MappingSpec newMappingSpec = neo4jClient.query(Objects.requireNonNull(cypherQuery, "Could not compute a query")) - .bindAll(finalParameters).fetchAs(preparedQuery.getResultType()); - return preparedQuery.getOptionalMappingFunction() - .map(newMappingSpec::mappedBy).or(() -> Optional.of(newMappingSpec)); + Neo4jClient.MappingSpec newMappingSpec = Neo4jTemplate.this.neo4jClient + .query(Objects.requireNonNull(cypherQuery, "Could not compute a query")) + .bindAll(finalParameters) + .fetchAs(this.preparedQuery.getResultType()); + return this.preparedQuery.getOptionalMappingFunction() + .map(newMappingSpec::mappedBy) + .or(() -> Optional.of(newMappingSpec)); } - private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, - QueryFragments queryFragments, Map parameters) { + private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsByIdStatementProvider( + Neo4jPersistentEntity entityMetaData, QueryFragments queryFragments, + Map parameters) { // first check if the root node(s) exist(s) at all - Statement rootNodesStatement = cypherGenerator - .prepareMatchOf(entityMetaData, queryFragments.getMatchOn(), queryFragments.getCondition()) - .returning(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).build(); + Statement rootNodesStatement = Neo4jTemplate.this.cypherGenerator + .prepareMatchOf(entityMetaData, queryFragments.getMatchOn(), queryFragments.getCondition()) + .returning(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) + .build(); Map usedParameters = new HashMap<>(parameters); usedParameters.putAll(rootNodesStatement.getCatalog().getParameters()); - final Collection rootNodeIds = new HashSet<>(neo4jClient - .query(renderer.render(rootNodesStatement)) - .bindAll(usedParameters) - .fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)) - .one() - .map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString)) - .orElseThrow()); + final Collection rootNodeIds = new HashSet<>( + Neo4jTemplate.this.neo4jClient.query(Neo4jTemplate.this.renderer.render(rootNodesStatement)) + .bindAll(usedParameters) + .fetchAs(Value.class) + .mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)) + .one() + .map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString)) + .orElseThrow()); if (rootNodeIds.isEmpty()) { // fast return if no matching root node(s) are found @@ -1377,84 +1515,106 @@ public final class Neo4jTemplate implements // load first level relationships final Map> relationshipsToRelatedNodeIds = new HashMap<>(); - for (RelationshipDescription relationshipDescription : entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField)) { + for (RelationshipDescription relationshipDescription : entityMetaData + .getRelationshipsInHierarchy(queryFragments::includeField)) { - Statement statement = cypherGenerator - .prepareMatchOf(entityMetaData, relationshipDescription, queryFragments.getMatchOn(), queryFragments.getCondition()) - .returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build(); + Statement statement = Neo4jTemplate.this.cypherGenerator + .prepareMatchOf(entityMetaData, relationshipDescription, queryFragments.getMatchOn(), + queryFragments.getCondition()) + .returning(Neo4jTemplate.this.cypherGenerator.createReturnStatementForMatch(entityMetaData)) + .build(); usedParameters = new HashMap<>(parameters); usedParameters.putAll(statement.getCatalog().getParameters()); - neo4jClient.query(renderer.render(statement)) - .bindAll(usedParameters) - .fetch() - .one() - .ifPresent(iterateAndMapNextLevel(relationshipsToRelatedNodeIds, relationshipDescription, PropertyPathWalkStep.empty())); + Neo4jTemplate.this.neo4jClient.query(Neo4jTemplate.this.renderer.render(statement)) + .bindAll(usedParameters) + .fetch() + .one() + .ifPresent(iterateAndMapNextLevel(relationshipsToRelatedNodeIds, relationshipDescription, + PropertyPathWalkStep.empty())); } - return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipsToRelatedNodeIds.keySet(), relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), queryFragments, elementIdOrIdFunction); + return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipsToRelatedNodeIds.keySet(), + relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), + queryFragments, Neo4jTemplate.this.elementIdOrIdFunction); } private void iterateNextLevel(Collection nodeIds, RelationshipDescription sourceRelationshipDescription, - Map> relationshipsToRelatedNodes, PropertyPathWalkStep currentPathStep) { + Map> relationshipsToRelatedNodes, PropertyPathWalkStep currentPathStep) { Neo4jPersistentEntity target = (Neo4jPersistentEntity) sourceRelationshipDescription.getTarget(); @SuppressWarnings("unchecked") - String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName(); + String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription) + .getInverse() + .getFieldName(); PropertyPathWalkStep nextPathStep; - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription.getRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription + .getRelationshipPropertiesEntity(); if (sourceRelationshipDescription.hasRelationshipProperties() && relationshipPropertiesEntity != null) { - var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + var targetNodeProperty = Objects.requireNonNull( + relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), + () -> "Could not get target node property on %s" + .formatted(relationshipPropertiesEntity.getType())); nextPathStep = currentPathStep.with(fieldName + "." + targetNodeProperty.getFieldName()); - } else { + } + else { nextPathStep = currentPathStep.with(fieldName); } Collection relationships = target - .getRelationshipsInHierarchy( - relaxedPropertyPath -> { + .getRelationshipsInHierarchy(relaxedPropertyPath -> { - PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path); - prepend = PropertyFilter.RelaxedPropertyPath.withRootType(preparedQuery.getResultType()).append(prepend.toDotPath()); - return preparedQuery.getQueryFragmentsAndParameters().getQueryFragments().includeField(prepend); - } - ); + PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path); + prepend = PropertyFilter.RelaxedPropertyPath.withRootType(this.preparedQuery.getResultType()) + .append(prepend.toDotPath()); + return this.preparedQuery.getQueryFragmentsAndParameters() + .getQueryFragments() + .includeField(prepend); + }); for (RelationshipDescription relationshipDescription : relationships) { Node node = anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(target)); - Statement statement = cypherGenerator - .prepareMatchOf(target, relationshipDescription, null, - elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_IDS))) - .returning(cypherGenerator.createGenericReturnStatement()).build(); + Statement statement = Neo4jTemplate.this.cypherGenerator + .prepareMatchOf(target, relationshipDescription, null, + Neo4jTemplate.this.elementIdOrIdFunction.apply(node) + .in(Cypher.parameter(Constants.NAME_OF_IDS))) + .returning(Neo4jTemplate.this.cypherGenerator.createGenericReturnStatement()) + .build(); - neo4jClient.query(renderer.render(statement)) - .bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, TemplateSupport.convertToLongIdOrStringElementId(nodeIds))) - .fetch() - .one() - .ifPresent(iterateAndMapNextLevel(relationshipsToRelatedNodes, relationshipDescription, nextPathStep)); + Neo4jTemplate.this.neo4jClient.query(Neo4jTemplate.this.renderer.render(statement)) + .bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, + TemplateSupport.convertToLongIdOrStringElementId(nodeIds))) + .fetch() + .one() + .ifPresent( + iterateAndMapNextLevel(relationshipsToRelatedNodes, relationshipDescription, nextPathStep)); } } - private Consumer> iterateAndMapNextLevel(Map> relationshipsToRelatedNodes, - RelationshipDescription relationshipDescription, - PropertyPathWalkStep currentPathStep) { + private Consumer> iterateAndMapNextLevel( + Map> relationshipsToRelatedNodes, RelationshipDescription relationshipDescription, + PropertyPathWalkStep currentPathStep) { return record -> { Map> relatedNodesVisited = new HashMap<>(relationshipsToRelatedNodes); @SuppressWarnings("unchecked") var sr = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS); - List newRelationshipIds = (sr != null) ? sr.stream().map(TemplateSupport::convertIdOrElementIdToString).toList() : List.of(); + List newRelationshipIds = (sr != null) + ? sr.stream().map(TemplateSupport::convertIdOrElementIdToString).toList() : List.of(); @SuppressWarnings("unchecked") var srn = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); - Set relatedIds = (srn != null) ? new HashSet<>(srn.stream().map(TemplateSupport::convertIdOrElementIdToString).toList()) : Set.of(); + Set relatedIds = (srn != null) + ? new HashSet<>(srn.stream().map(TemplateSupport::convertIdOrElementIdToString).toList()) + : Set.of(); // use this list to get down the road // 1. remove already visited ones; - // we don't know which id came with which node, so we need to assume that a relationshipId connects to all related nodes + // we don't know which id came with which node, so we need to assume that + // a relationshipId connects to all related nodes for (String newRelationshipId : newRelationshipIds) { relatedNodesVisited.put(newRelationshipId, relatedIds); Set knownRelatedNodesBefore = relationshipsToRelatedNodes.get(newRelationshipId); @@ -1474,5 +1634,7 @@ public final class Neo4jTemplate implements } }; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java index 64488219d..4e5484cf7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -15,20 +15,6 @@ */ package org.springframework.data.neo4j.core; -import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; -import org.neo4j.driver.Record; -import org.neo4j.driver.Value; -import org.neo4j.driver.Values; -import org.neo4j.driver.types.MapAccessor; -import org.neo4j.driver.types.Node; -import org.neo4j.driver.types.Path; -import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.mapping.Constants; -import org.springframework.data.neo4j.core.mapping.MappingSupport; -import org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException; -import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; - import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -44,31 +30,46 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import org.neo4j.driver.Record; +import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.Node; +import org.neo4j.driver.types.Path; +import org.neo4j.driver.types.TypeSystem; + +import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.MappingSupport; +import org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; + /** - * Typed preparation of a query that is used to create either an executable query. Executable queries come in two - * fashions: imperative and reactive. Depending on which client is used to retrieve one, you get one or the other. + * Typed preparation of a query that is used to create either an executable query. + * Executable queries come in two fashions: imperative and reactive. Depending on which + * client is used to retrieve one, you get one or the other. *

- * When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure - * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is - * expected by the mapping function to work correctly. + * When no mapping function is provided, the Neo4j client will assume a simple type to be + * returned. Otherwise, make sure that the query fits to the mapping function, that is: It + * must return all nodes, relationships and paths that is expected by the mapping function + * to work correctly. * + * @param the type of the objects returned by this query. * @author Michael J. Simons * @author Gerrit Meier - * @param The type of the objects returned by this query. - * @soundtrack Deichkind - Arbeit nervt * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") public final class PreparedQuery { - public static RequiredBuildStep queryFor(Class resultType) { - return new RequiredBuildStep<>(resultType); - } - private final Class resultType; + private final QueryFragmentsAndParameters queryFragmentsAndParameters; + @Nullable private final Supplier> mappingFunctionSupplier; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private volatile Optional> lastMappingFunction = Optional.empty(); @@ -78,24 +79,27 @@ public final class PreparedQuery { this.queryFragmentsAndParameters = optionalBuildSteps.queryFragmentsAndParameters; } + public static RequiredBuildStep queryFor(Class resultType) { + return new RequiredBuildStep<>(resultType); + } + public Class getResultType() { return this.resultType; } @SuppressWarnings("unchecked") public synchronized Optional> getOptionalMappingFunction() { - lastMappingFunction = Optional.ofNullable(this.mappingFunctionSupplier) - .map(Supplier::get) - .map(f -> (BiFunction) new AggregatingMappingFunction(f)); - return lastMappingFunction; + this.lastMappingFunction = Optional.ofNullable(this.mappingFunctionSupplier) + .map(Supplier::get) + .map(f -> (BiFunction) new AggregatingMappingFunction(f)); + return this.lastMappingFunction; } synchronized boolean resultsHaveBeenAggregated() { - return lastMappingFunction - .filter(AggregatingMappingFunction.class::isInstance) - .map(AggregatingMappingFunction.class::cast) - .map(AggregatingMappingFunction::hasAggregated) - .orElse(false); + return this.lastMappingFunction.filter(AggregatingMappingFunction.class::isInstance) + .map(AggregatingMappingFunction.class::cast) + .map(AggregatingMappingFunction::hasAggregated) + .orElse(false); } public QueryFragmentsAndParameters getQueryFragmentsAndParameters() { @@ -103,10 +107,13 @@ public final class PreparedQuery { } /** - * @param The concrete type of this build step. + * Step configuring the query to be used. + * + * @param the concrete type of this build step. * @since 6.0 */ - public static class RequiredBuildStep { + public static final class RequiredBuildStep { + private final Class resultType; private RequiredBuildStep(Class resultType) { @@ -114,22 +121,28 @@ public final class PreparedQuery { } public OptionalBuildSteps withCypherQuery(String cypherQuery) { - return new OptionalBuildSteps<>(resultType, new QueryFragmentsAndParameters(cypherQuery)); + return new OptionalBuildSteps<>(this.resultType, new QueryFragmentsAndParameters(cypherQuery)); } - public OptionalBuildSteps withQueryFragmentsAndParameters(QueryFragmentsAndParameters queryFragmentsAndParameters) { - return new OptionalBuildSteps<>(resultType, queryFragmentsAndParameters); + public OptionalBuildSteps withQueryFragmentsAndParameters( + QueryFragmentsAndParameters queryFragmentsAndParameters) { + return new OptionalBuildSteps<>(this.resultType, queryFragmentsAndParameters); } + } /** - * @param The concrete type of this build step. + * Step configuring parameters or mapping functions. + * + * @param the concrete type of this build step. * @since 6.0 */ - public static class OptionalBuildSteps { + public static final class OptionalBuildSteps { final Class resultType; + final QueryFragmentsAndParameters queryFragmentsAndParameters; + @Nullable Supplier> mappingFunctionSupplier; @@ -140,16 +153,16 @@ public final class PreparedQuery { /** * This replaces the current parameters. - * - * @param newParameters The new parameters for the prepared query. - * @return This builder. + * @param newParameters the new parameters for the prepared query. + * @return this builder */ public OptionalBuildSteps withParameters(@Nullable Map newParameters) { this.queryFragmentsAndParameters.setParameters(Objects.requireNonNullElseGet(newParameters, Map::of)); return this; } - public OptionalBuildSteps usingMappingFunction(@Nullable Supplier> newMappingFunction) { + public OptionalBuildSteps usingMappingFunction( + @Nullable Supplier> newMappingFunction) { this.mappingFunctionSupplier = newMappingFunction; return this; } @@ -157,11 +170,13 @@ public final class PreparedQuery { public PreparedQuery build() { return new PreparedQuery<>(this); } + } private static class AggregatingMappingFunction implements BiFunction { private final BiFunction target; + private final AtomicBoolean aggregated = new AtomicBoolean(false); AggregatingMappingFunction(BiFunction target) { @@ -173,19 +188,19 @@ public final class PreparedQuery { if (MappingSupport.isListContainingOnly(t.LIST(), t.PATH()).test(value)) { return new LinkedHashSet(aggregatePath(t, value, Collections.emptyList())); } - return value.asList(v -> target.apply(t, v)); + return value.asList(v -> this.target.apply(t, v)); } private Collection aggregatePath(TypeSystem t, Value value, List> additionalValues) { - // We are using linked hash sets here so that the order of nodes will be stable and match that of the path. + // We are using linked hash sets here so that the order of nodes will be + // stable and match that of the path. Set result = new LinkedHashSet<>(); Set nodes = new LinkedHashSet<>(); Set relationships = new LinkedHashSet<>(); - List paths = value.hasType(t.PATH()) - ? Collections.singletonList(value.asPath()) + List paths = value.hasType(t.PATH()) ? Collections.singletonList(value.asPath()) : value.asList(Value::asPath); for (Path path : paths) { @@ -211,10 +226,12 @@ public final class PreparedQuery { } } - // This loop synthesizes a node, it's relationship and all related nodes for all nodes in a path. + // This loop synthesizes a node, it's relationship and all related nodes for + // all nodes in a path. // All other nodes must be assumed to somehow related Map mapValue = new HashMap<>(); - // Those values and the combinations with the relationships will stay constant for each node in question + // Those values and the combinations with the relationships will stay constant + // for each node in question additionalValues.forEach(e -> mapValue.put(e.getKey(), e.getValue())); mapValue.put(Constants.NAME_OF_SYNTHESIZED_RELATIONS, Values.value(relationships)); mapValue.put(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES, Values.value(nodes)); @@ -222,9 +239,11 @@ public final class PreparedQuery { for (Value rootNode : nodes) { mapValue.put(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE, rootNode); try { - result.add(target.apply(t, Values.value(mapValue))); - } catch (NoRootNodeMappingException e) { - // This is the case for nodes on the path that are not of the target type + result.add(this.target.apply(t, Values.value(mapValue))); + } + catch (NoRootNodeMappingException ex) { + // This is the case for nodes on the path that are not of the target + // type // We can safely ignore those. } } @@ -241,33 +260,39 @@ public final class PreparedQuery { if (r.size() == 1) { Value value = r.get(0); if (value.hasType(t.LIST())) { - aggregated.compareAndSet(false, true); + this.aggregated.compareAndSet(false, true); return aggregateList(t, value); - } else if (value.hasType(t.PATH())) { - aggregated.compareAndSet(false, true); + } + else if (value.hasType(t.PATH())) { + this.aggregated.compareAndSet(false, true); return aggregatePath(t, value, Collections.emptyList()); } } try { - return target.apply(t, r); - } catch (NoRootNodeMappingException e) { + return this.target.apply(t, r); + } + catch (NoRootNodeMappingException ex) { - // We didn't find anything on the top level. It still can be a path plus some additional information + // We didn't find anything on the top level. It still can be a path plus + // some additional information // to enrich the nodes on the path with. - Map>> pathValues = r.asMap(Function.identity()).entrySet() - .stream() - .collect(Collectors.partitioningBy(entry -> entry.getValue().hasType(t.PATH()))); + Map>> pathValues = r.asMap(Function.identity()) + .entrySet() + .stream() + .collect(Collectors.partitioningBy(entry -> entry.getValue().hasType(t.PATH()))); if (pathValues.get(true).size() == 1) { - aggregated.compareAndSet(false, true); + this.aggregated.compareAndSet(false, true); return aggregatePath(t, pathValues.get(true).get(0).getValue(), pathValues.get(false)); } - throw e; + throw ex; } } boolean hasAggregated() { - return aggregated.get(); + return this.aggregated.get(); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java index 376691fa2..ffe4add2a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java @@ -15,7 +15,15 @@ */ package org.springframework.data.neo4j.core; +import java.beans.PropertyDescriptor; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; + import org.apiguardian.api.API; + import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription; @@ -29,22 +37,21 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; -import java.beans.PropertyDescriptor; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Optional; - /** - * This class is responsible for creating a List of {@link PropertyPath} entries that contains all reachable - * properties (w/o circles). + * This class is responsible for creating a List of {@link PropertyPath} entries that + * contains all reachable properties (w/o circles). + * + * @author Michael J. Simons + * @author Gerrit Meier */ @API(status = API.Status.INTERNAL, since = "6.1.3") public final class PropertyFilterSupport { - public static Collection getInputProperties(ResultProcessor resultProcessor, ProjectionFactory factory, - Neo4jMappingContext mappingContext) { + private PropertyFilterSupport() { + } + + public static Collection getInputProperties(ResultProcessor resultProcessor, + ProjectionFactory factory, Neo4jMappingContext mappingContext) { ReturnedType returnedType = resultProcessor.getReturnedType(); Class potentiallyProjectedType = returnedType.getReturnedType(); @@ -60,20 +67,25 @@ public final class PropertyFilterSupport { } for (String inputProperty : returnedType.getInputProperties()) { - addPropertiesFrom(domainType, potentiallyProjectedType, factory, - filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, potentiallyProjectedType).getLeafProperty().getTypeInformation()), mappingContext); + addPropertiesFrom(domainType, potentiallyProjectedType, factory, filteredProperties, + new ProjectionPathProcessor(inputProperty, + PropertyPath.from(inputProperty, potentiallyProjectedType) + .getLeafProperty() + .getTypeInformation()), + mappingContext); } for (String inputProperty : KPropertyFilterSupport.getRequiredProperties(domainType)) { - addPropertiesFrom(domainType, potentiallyProjectedType, factory, - filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, domainType).getLeafProperty().getTypeInformation()), mappingContext); + addPropertiesFrom(domainType, potentiallyProjectedType, factory, filteredProperties, + new ProjectionPathProcessor(inputProperty, + PropertyPath.from(inputProperty, domainType).getLeafProperty().getTypeInformation()), + mappingContext); } return filteredProperties; } static Collection addPropertiesFrom(Class domainType, Class returnType, - ProjectionFactory projectionFactory, - Neo4jMappingContext neo4jMappingContext) { + ProjectionFactory projectionFactory, Neo4jMappingContext neo4jMappingContext) { ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType); Collection propertyPaths = new HashSet<>(); @@ -83,11 +95,15 @@ public final class PropertyFilterSupport { TypeInformation typeInformation = null; if (projectionInformation.isClosed()) { typeInformation = PropertyPath.from(inputProperty.getName(), returnType).getTypeInformation(); - } else { + } + else { // try to figure out the right property by name for (GraphPropertyDescription graphProperty : domainEntity.getGraphProperties()) { if (graphProperty.getPropertyName().equals(inputProperty.getName())) { - typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(graphProperty.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null); + typeInformation = Optional + .ofNullable(domainEntity.getPersistentProperty(graphProperty.getFieldName())) + .map(PersistentProperty::getTypeInformation) + .orElse(null); break; } } @@ -95,33 +111,41 @@ public final class PropertyFilterSupport { if (typeInformation == null) { for (RelationshipDescription relationshipDescription : domainEntity.getRelationships()) { if (relationshipDescription.getFieldName().equals(inputProperty.getName())) { - typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(relationshipDescription.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null); + typeInformation = Optional + .ofNullable(domainEntity.getPersistentProperty(relationshipDescription.getFieldName())) + .map(PersistentProperty::getTypeInformation) + .orElse(null); break; } } } } if (typeInformation != null) { - addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext); + addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, + new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext); } } return propertyPaths; } private static void addPropertiesFrom(Class domainType, Class returnedType, ProjectionFactory factory, - Collection filteredProperties, ProjectionPathProcessor projectionPathProcessor, - Neo4jMappingContext mappingContext) { + Collection filteredProperties, + ProjectionPathProcessor projectionPathProcessor, Neo4jMappingContext mappingContext) { ProjectionInformation projectionInformation = factory.getProjectionInformation(returnedType); PropertyFilter.RelaxedPropertyPath propertyPath; - // If this is a closed projection we can assume that the return type (possible projection type) contains + // If this is a closed projection we can assume that the return type (possible + // projection type) contains // only fields accessible with a property path. if (projectionInformation.isClosed()) { - propertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(returnedType).append(projectionPathProcessor.path); - } else { + propertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(returnedType) + .append(projectionPathProcessor.path); + } + else { // otherwise the domain type is used right from the start - propertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(domainType).append(projectionPathProcessor.path); + propertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(domainType) + .append(projectionPathProcessor.path); } Class propertyType = projectionPathProcessor.typeInformation.getType(); @@ -130,13 +154,17 @@ public final class PropertyFilterSupport { // deep inspection into the map to look for the related entity type. TypeInformation mapValueType = projectionPathProcessor.typeInformation.getRequiredMapValueType(); if (mapValueType.isCollectionLike()) { - currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType().getComponentType(); - propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType(); - } else { + currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType() + .getComponentType(); + propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type") + .getType(); + } + else { currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType(); propertyType = currentTypeInformation.getType(); } - } else if (projectionPathProcessor.typeInformation.isCollectionLike()) { + } + else if (projectionPathProcessor.typeInformation.isCollectionLike()) { currentTypeInformation = projectionPathProcessor.typeInformation.getComponentType(); propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType(); } @@ -148,45 +176,64 @@ public final class PropertyFilterSupport { // 3. Embedded projection if (mappingContext.getConversionService().isSimpleType(propertyType)) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, false)); - } else if (mappingContext.hasPersistentEntityFor(propertyType)) { + } + else if (mappingContext.hasPersistentEntityFor(propertyType)) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, true)); - } else { + } + else { ProjectionInformation nestedProjectionInformation = factory.getProjectionInformation(propertyType); // Closed projection should get handled as above (recursion) if (nestedProjectionInformation.isClosed()) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, false)); for (PropertyDescriptor nestedInputProperty : nestedProjectionInformation.getInputProperties()) { - TypeInformation typeInformation = currentTypeInformation.getRequiredProperty(nestedInputProperty.getName()); - ProjectionPathProcessor nextProjectionPathProcessor = projectionPathProcessor.next(nestedInputProperty, typeInformation); + TypeInformation typeInformation = currentTypeInformation + .getRequiredProperty(nestedInputProperty.getName()); + ProjectionPathProcessor nextProjectionPathProcessor = projectionPathProcessor + .next(nestedInputProperty, typeInformation); - TypeInformation actualType = Objects.requireNonNull(nextProjectionPathProcessor.typeInformation.getActualType()); - if (projectionPathProcessor.isChildLevel() && - (domainType.equals(nextProjectionPathProcessor.typeInformation.getType()) - || returnedType.equals(actualType.getType()) - || returnedType.equals(nextProjectionPathProcessor.typeInformation.getType()))) { + TypeInformation actualType = Objects + .requireNonNull(nextProjectionPathProcessor.typeInformation.getActualType()); + if (projectionPathProcessor.isChildLevel() + && (domainType.equals(nextProjectionPathProcessor.typeInformation.getType()) + || returnedType.equals(actualType.getType()) + || returnedType.equals(nextProjectionPathProcessor.typeInformation.getType()))) { break; } - if (projectionPathProcessor.typeInformation.getActualType() != null && projectionPathProcessor.typeInformation.getActualType().getType().equals(actualType.getType()) - || (!projectionPathProcessor.typeInformation.isCollectionLike() && !projectionPathProcessor.typeInformation.isMap() && projectionPathProcessor.typeInformation.getType().equals(nextProjectionPathProcessor.typeInformation.getType()))) { + if (projectionPathProcessor.typeInformation.getActualType() != null + && projectionPathProcessor.typeInformation.getActualType() + .getType() + .equals(actualType.getType()) + || (!projectionPathProcessor.typeInformation.isCollectionLike() + && !projectionPathProcessor.typeInformation.isMap() + && projectionPathProcessor.typeInformation.getType() + .equals(nextProjectionPathProcessor.typeInformation.getType()))) { filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, true)); - } else { + } + else { addPropertiesFrom(domainType, returnedType, factory, filteredProperties, nextProjectionPathProcessor, mappingContext); } } - } else { - // An open projection at this place needs to get replaced with the matching (real) entity + } + else { + // An open projection at this place needs to get replaced with the + // matching (real) entity // Use domain type as root type for the property path - PropertyFilter.RelaxedPropertyPath domainBasedPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(domainType).append(projectionPathProcessor.path); + PropertyFilter.RelaxedPropertyPath domainBasedPropertyPath = PropertyFilter.RelaxedPropertyPath + .withRootType(domainType) + .append(projectionPathProcessor.path); filteredProperties.add(new PropertyFilter.ProjectedPath(domainBasedPropertyPath, true)); } } } - private static class ProjectionPathProcessor { + private static final class ProjectionPathProcessor { + final TypeInformation typeInformation; + final String path; + final String name; private ProjectionPathProcessor(String name, String path, TypeInformation typeInformation) { @@ -199,14 +246,16 @@ public final class PropertyFilterSupport { this(name, name, typeInformation); } - public ProjectionPathProcessor next(PropertyDescriptor nextProperty, TypeInformation nextTypeInformation) { + ProjectionPathProcessor next(PropertyDescriptor nextProperty, TypeInformation nextTypeInformation) { String nextPropertyName = nextProperty.getName(); - return new ProjectionPathProcessor(nextPropertyName, path + "." + nextPropertyName, nextTypeInformation); + return new ProjectionPathProcessor(nextPropertyName, this.path + "." + nextPropertyName, + nextTypeInformation); } - public boolean isChildLevel() { - return path.contains("."); + boolean isChildLevel() { + return this.path.contains("."); } + } } diff --git a/src/main/java/org/springframework/data/neo4j/core/PropertyPathWalkStep.java b/src/main/java/org/springframework/data/neo4j/core/PropertyPathWalkStep.java index 96b0dc769..c01c94d33 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PropertyPathWalkStep.java +++ b/src/main/java/org/springframework/data/neo4j/core/PropertyPathWalkStep.java @@ -18,23 +18,27 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; /** - * Wrapper class for simple propertyPath specific modification. - * Returns new instances on modification and hides the ugly empty String. + * Wrapper class for simple propertyPath specific modification. Returns new instances on + * modification and hides the ugly empty String. + * + * @author Gerrit Meier + * @author Michael J. Simons */ @API(status = API.Status.INTERNAL) -class PropertyPathWalkStep { +final class PropertyPathWalkStep { final String path; + private PropertyPathWalkStep(String path) { + this.path = path; + } + static PropertyPathWalkStep empty() { return new PropertyPathWalkStep(""); } - public PropertyPathWalkStep with(String addition) { - return new PropertyPathWalkStep(path.isEmpty() ? addition : path + "." + addition); + PropertyPathWalkStep with(String addition) { + return new PropertyPathWalkStep(this.path.isEmpty() ? addition : this.path + "." + addition); } - private PropertyPathWalkStep(String path) { - this.path = path; - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveDatabaseSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveDatabaseSelectionProvider.java index 5f054a235..06ef5d058 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveDatabaseSelectionProvider.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveDatabaseSelectionProvider.java @@ -15,17 +15,17 @@ */ package org.springframework.data.neo4j.core; +import org.apiguardian.api.API; import reactor.core.publisher.Mono; -import org.apiguardian.api.API; import org.springframework.util.Assert; /** - * This is the reactive version of a the {@link DatabaseSelectionProvider} and it works in the same way but uses - * reactive return types containing the target database name. An empty mono indicates the default database. + * This is the reactive version of a the {@link DatabaseSelectionProvider} and it works in + * the same way but uses reactive return types containing the target database name. An + * empty mono indicates the default database. * * @author Michael J. Simons - * @soundtrack Rage - Reign Of Fear * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -33,16 +33,10 @@ import org.springframework.util.Assert; public interface ReactiveDatabaseSelectionProvider { /** - * @return The selected database to interact with. - */ - Mono getDatabaseSelection(); - - /** - * Creates a statically configured database selection provider always selecting the database with the given name - * {@code databaseName}. - * - * @param databaseName The database name to use, must not be null nor empty. - * @return A statically configured database name provider. + * Creates a statically configured database selection provider always selecting the + * database with the given name {@code databaseName}. + * @param databaseName the database name to use, must not be null nor empty. + * @return a statically configured database name provider. */ static ReactiveDatabaseSelectionProvider createStaticDatabaseSelectionProvider(String databaseName) { @@ -54,20 +48,17 @@ public interface ReactiveDatabaseSelectionProvider { /** * A database selector always selecting the default database. - * - * @return A provider for the default database name. + * @return a provider for the default database name. */ static ReactiveDatabaseSelectionProvider getDefaultSelectionProvider() { return DefaultReactiveDatabaseSelectionProvider.INSTANCE; } -} -enum DefaultReactiveDatabaseSelectionProvider implements ReactiveDatabaseSelectionProvider { - INSTANCE; + /** + * Returns the selected database to interact with. + * @return the selected database to interact with + */ + Mono getDatabaseSelection(); - @Override - public Mono getDatabaseSelection() { - return Mono.just(DatabaseSelection.undecided()); - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java index 2c48969b2..f96ed7cd6 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java @@ -15,23 +15,23 @@ */ package org.springframework.data.neo4j.core; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Collections; import java.util.Map; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; /** - * {@link ReactiveFluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style. - *
- * The starting {@literal domainType} is used for mapping the query provided via {@code by} into the - * Neo4j specific representation. By default, the originating {@literal domainType} is also used for mapping back the - * result. However, it is possible to define a different {@literal returnType} via - * {@code as} to mapping the result.
+ * {@link ReactiveFluentFindOperation} allows creation and execution of Neo4j find + * operations in a fluent API style.
+ * The starting {@literal domainType} is used for mapping the query provided via + * {@code by} into the Neo4j specific representation. By default, the originating + * {@literal domainType} is also used for mapping back the result. However, it is possible + * to define a different {@literal returnType} via {@code as} to mapping the result.
* * @author Michael Simons * @since 6.1 @@ -41,15 +41,16 @@ public interface ReactiveFluentFindOperation { /** * Start creating a find operation for the given {@literal domainType}. - * * @param domainType must not be {@literal null}. + * @param the domain type * @return new instance of {@link ExecutableFind}. * @throws IllegalArgumentException if domainType is {@literal null}. */ ExecutableFind find(Class domainType); /** - * Trigger find execution by calling one of the terminating methods from a state where no query is yet defined. + * Trigger find execution by calling one of the terminating methods from a state where + * no query is yet defined. * * @param returned type */ @@ -57,10 +58,10 @@ public interface ReactiveFluentFindOperation { /** * Get all matching elements. - * * @return never {@literal null}. */ Flux all(); + } /** @@ -72,11 +73,12 @@ public interface ReactiveFluentFindOperation { /** * Get exactly zero or one result. - * - * @return A publisher containing one or no result - * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @return a publisher containing one or no result + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more + * than one match found. */ Mono one(); + } /** @@ -88,27 +90,26 @@ public interface ReactiveFluentFindOperation { /** * Set the filter query to be used. - * * @param query must not be {@literal null}. - * @param parameter Optional parameter map + * @param parameter optional parameter map * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. */ TerminatingFind matching(String query, Map parameter); /** - * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code - * and we actively discourage using this method. - * - * @param queryFragmentsAndParameters Encapsulated query fragments and parameters as created by the repository abstraction. + * Creates an executable query based on fragments and parameters. Hardly useful + * outside framework-code and we actively discourage using this method. + * @param queryFragmentsAndParameters encapsulated query fragments and parameters + * as created by the repository abstraction. * @return new instance of {@link FluentFindOperation.TerminatingFind}. - * @throws IllegalArgumentException if queryFragmentsAndParameters is {@literal null}. + * @throws IllegalArgumentException if queryFragmentsAndParameters is + * {@literal null}. */ TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters); /** * Set the filter query to be used. - * * @param query must not be {@literal null}. * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if query is {@literal null}. @@ -119,9 +120,9 @@ public interface ReactiveFluentFindOperation { /** * Set the filter {@link Statement statement} to be used. - * * @param statement must not be {@literal null}. - * @param parameter Will be merged with parameters in the statement. Parameters in {@code parameter} have precedence. + * @param parameter will be merged with parameters in the statement. Parameters in + * {@code parameter} have precedence. * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if statement is {@literal null}. */ @@ -129,7 +130,6 @@ public interface ReactiveFluentFindOperation { /** * Set the filter {@link Statement statement} to be used. - * * @param statement must not be {@literal null}. * @return new instance of {@link TerminatingFind}. * @throws IllegalArgumentException if criteria is {@literal null}. @@ -137,6 +137,7 @@ public interface ReactiveFluentFindOperation { default TerminatingFind matching(Statement statement) { return matching(statement, Collections.emptyMap()); } + } /** @@ -149,13 +150,13 @@ public interface ReactiveFluentFindOperation { /** * Define the target type fields should be mapped to.
* Skip this step if you are anyway only interested in the original domain type. - * * @param resultType must not be {@literal null}. - * @param result type. + * @param result type. * @return new instance of {@link FindWithProjection}. * @throws IllegalArgumentException if resultType is {@literal null}. */ FindWithQuery as(Class resultType); + } /** @@ -164,5 +165,7 @@ public interface ReactiveFluentFindOperation { * @param returned type */ interface ExecutableFind extends FindWithProjection { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentNeo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentNeo4jOperations.java index b44b0411c..f4b06029d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentNeo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentNeo4jOperations.java @@ -18,13 +18,13 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; /** - * An additional interface accompanying the {@link ReactiveNeo4jOperations} and adding a couple of fluent operations, especially - * around finding and projecting things. + * An additional interface accompanying the {@link ReactiveNeo4jOperations} and adding a + * couple of fluent operations, especially around finding and projecting things. * * @author Michael J. Simons - * @soundtrack Ozzy Osbourne - Ordinary Man * @since 6.1 */ @API(status = API.Status.STABLE, since = "6.1") public interface ReactiveFluentNeo4jOperations extends ReactiveFluentFindOperation, ReactiveFluentSaveOperation { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java index ddb4aa631..a548075f3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java @@ -15,14 +15,14 @@ */ package org.springframework.data.neo4j.core; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Collections; import java.util.Map; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; @@ -30,7 +30,6 @@ import org.springframework.util.Assert; * Implementation of {@link ReactiveFluentFindOperation}. * * @author Michael J. Simons - * @soundtrack Ozzy Osbourne - Ordinary Man * @since 6.1 */ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperation, ReactiveFluentSaveOperation { @@ -46,24 +45,36 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableFindSupport<>(template, domainType, domainType, null, Collections.emptyMap()); + return new ExecutableFindSupport<>(this.template, domainType, domainType, null, Collections.emptyMap()); + } + + @Override + public ExecutableSave save(Class domainType) { + Assert.notNull(domainType, "DomainType must not be null"); + + return new ExecutableSaveSupport<>(this.template, domainType); } private static class ExecutableFindSupport implements ExecutableFind, FindWithProjection, FindWithQuery, TerminatingFind { private final ReactiveNeo4jTemplate template; + private final Class domainType; + private final Class returnType; + @Nullable private final String query; + @Nullable private final Map parameters; + @Nullable private final QueryFragmentsAndParameters queryFragmentsAndParameters; - ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, @Nullable String query, - @Nullable Map parameters) { + ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, + @Nullable String query, @Nullable Map parameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -72,7 +83,8 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio this.queryFragmentsAndParameters = null; } - ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { + ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, + @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -87,7 +99,7 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio Assert.notNull(returnType, "ReturnType must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); + return new ExecutableFindSupport<>(this.template, this.domainType, returnType, this.query, this.parameters); } @Override @@ -96,20 +108,21 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio Assert.notNull(query, "Query must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); + return new ExecutableFindSupport<>(this.template, this.domainType, this.returnType, query, parameters); } @Override @SuppressWarnings("HiddenField") public TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters) { - return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters); + return new ExecutableFindSupport<>(this.template, this.domainType, this.returnType, + queryFragmentsAndParameters); } @Override public TerminatingFind matching(Statement statement, Map parameter) { - return matching(template.render(statement), TemplateSupport.mergeParameters(statement, parameter)); + return matching(this.template.render(statement), TemplateSupport.mergeParameters(statement, parameter)); } @Override @@ -123,20 +136,16 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio } private Flux doFind(TemplateSupport.FetchType fetchType) { - return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters); + return this.template.doFind(this.query, this.parameters, this.domainType, this.returnType, fetchType, + this.queryFragmentsAndParameters); } - } - @Override - public ExecutableSave save(Class domainType) { - Assert.notNull(domainType, "DomainType must not be null"); - - return new ExecutableSaveSupport<>(this.template, domainType); } private static class ExecutableSaveSupport
implements ReactiveFluentSaveOperation.ExecutableSave
{ private final ReactiveNeo4jTemplate template; + private final Class
domainType; ExecutableSaveSupport(ReactiveNeo4jTemplate template, Class
domainType) { @@ -157,7 +166,9 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio } private Flux doSave(Iterable instances) { - return template.doSave(instances, domainType); + return this.template.doSave(instances, this.domainType); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentSaveOperation.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentSaveOperation.java index ac3f4fa93..bc37f7df8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentSaveOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentSaveOperation.java @@ -15,18 +15,19 @@ */ package org.springframework.data.neo4j.core; +import org.apiguardian.api.API; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.apiguardian.api.API; - /** - * {@link ReactiveFluentSaveOperation} allows creation and execution of Neo4j save operations in a fluent API style. It - * is designed to be used together with the {@link FluentFindOperation fluent find operations}. + * {@link ReactiveFluentSaveOperation} allows creation and execution of Neo4j save + * operations in a fluent API style. It is designed to be used together with the + * {@link FluentFindOperation fluent find operations}. *

- * Both interfaces provide a way to specify a pair of two types: A domain type and a result (projected) type. - * The fluent save operations are mainly used with DTO based projections. Closed interface projections won't be that - * helpful when you received them via {@link FluentFindOperation fluent find operations} as they won't be modifiable. + * Both interfaces provide a way to specify a pair of two types: A domain type and a + * result (projected) type. The fluent save operations are mainly used with DTO based + * projections. Closed interface projections won't be that helpful when you received them + * via {@link FluentFindOperation fluent find operations} as they won't be modifiable. * * @author Michael J. Simons * @since 6.2 @@ -36,36 +37,43 @@ public interface ReactiveFluentSaveOperation { /** * Start creating a save operation for the given {@literal domainType}. - * * @param domainType must not be {@literal null}. + * @param the domain type * @return new instance of {@link ExecutableSave}. * @throws IllegalArgumentException if domainType is {@literal null}. */ ExecutableSave save(Class domainType); /** - * After the domain type has been specified, related projections or instances of the domain type can be saved. + * After the domain type has been specified, related projections or instances of the + * domain type can be saved. * * @param

the domain type */ interface ExecutableSave
{ /** - * @param instance The instance to be saved - * @param The type of the instance passed to this method. It should be the same as the domain type before - * or a projection of the domain type. If they are not related, the results may be undefined. - * @return The saved instance, can also be a new object, so you are recommended to use this instance after - * the save operation + * Saves exactly one instance. + * @param instance the instance to be saved + * @param the type of the instance passed to this method. It should be the + * same as the domain type before or a projection of the domain type. If they are + * not related, the results may be undefined. + * @return the saved instance, can also be a new object, so you are recommended to + * use this instance after the save operation */ Mono one(T instance); /** - * @param instances The instances to be saved - * @param The type of the instances passed to this method. It should be the same as the domain type before - * or a projection of the domain type. If they are not related, the results may be undefined. - * @return The saved instances, can also be a new objects, so you are recommended to use those instances - * after the save operation + * Saves several instances. + * @param instances the instances to be saved + * @param the type of the instances passed to this method. It should be the + * same as the domain type before or a projection of the domain type. If they are + * not related, the results may be undefined. + * @return the saved instances, can also be a new objects, so you are recommended + * to use those instances after the save operation */ Flux all(Iterable instances); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java index 0baa56a74..efc66ae0f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.core; -import org.jspecify.annotations.Nullable; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; @@ -27,27 +22,39 @@ import java.util.function.Supplier; import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.reactivestreams.ReactiveQueryRunner; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; /** - * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all - * operations will only be executed once something subscribes to the reactive sequence defined. + * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j + * client} is the fact that all operations will only be executed once something subscribes + * to the reactive sequence defined. * * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Im Auftrag des Herrn * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface ReactiveNeo4jClient { + /** + * All Cypher statements executed will be logged here. + */ LogAccessor cypherLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher")); + + /** + * Some methods of the {@link ReactiveNeo4jClient} will be logged here. + */ LogAccessor log = new LogAccessor(LogFactory.getLog(ReactiveNeo4jClient.class)); static ReactiveNeo4jClient create(Driver driver) { @@ -65,12 +72,258 @@ public interface ReactiveNeo4jClient { return new Builder(driver); } + /** + * Retrieves a query runner matching the plain Neo4j Java Driver api bound to Spring * + * transactions. + * @return a managed query runner + * @since 6.2 + * @see #getQueryRunner(Mono) + */ + default Mono getQueryRunner() { + return getQueryRunner(Mono.just(DatabaseSelection.undecided())); + } + + /** + * Retrieves a query runner matching the plain Neo4j Java Driver api bound to Spring * + * transactions configured to use a specific database. + * @param databaseSelection the database to use + * @return a managed query runner + * @since 6.2 + * @see #getQueryRunner(Mono, Mono) + */ + default Mono getQueryRunner(Mono databaseSelection) { + return getQueryRunner(databaseSelection, Mono.just(UserSelection.connectedUser())); + } + + /** + * Retrieves a query runner that will participate in ongoing Spring transactions + * (either in declarative (implicit via {@code @Transactional}) or in programmatically + * (explicit via transaction template) ones). This runner can be used with the + * Cypher-DSL for example. If the client cannot retrieve an ongoing Spring + * transaction, this runner will use auto-commit semantics. + * @param databaseSelection the target database. + * @param userSelection the user selection + * @return a managed query runner + * @since 6.2 + */ + Mono getQueryRunner(Mono databaseSelection, + Mono userSelection); + + /** + * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether + * it's a match, merge, create or removal of things. + * @param cypher the cypher code that shall be executed + * @return a new CypherSpec + */ + UnboundRunnableSpec query(String cypher); + + /** + * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at + * this point whether it's a match, merge, create or removal of things. The supplier + * can be an arbitrary Supplier that may provide a DSL for generating the Cypher + * statement. + * @param cypherSupplier a supplier of arbitrary Cypher code + * @return a runnable query specification. + */ + UnboundRunnableSpec query(Supplier cypherSupplier); + + /** + * Delegates interaction with the default database to the given callback. + * @param callback a function receiving a reactive statement runner for database + * interaction that can optionally return a publisher with none or exactly one element + * @param the type of the result being produced + * @return a single publisher containing none or exactly one element that will be + * produced by the callback + */ + OngoingDelegation delegateTo(Function> callback); + + /** + * Returns the assigned database selection provider. + * @return the database selection provider - can be null + */ + @Nullable ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider(); + + /** + * Step for defining the mapping. + * + * @param the resulting type of this mapping + * @since 6.0 + */ + interface MappingSpec extends RecordFetchSpec { + + /** + * The mapping function is responsible to turn one record into one domain object. + * It will receive the record itself and in addition, the type system that the + * Neo4j Java-Driver used while executing the query. + * @param mappingFunction the mapping function used to create new domain objects + * @return a specification how to fetch one or more records. + */ + RecordFetchSpec mappedBy(BiFunction mappingFunction); + + } + + /** + * Final step that triggers fetching. + * + * @param the type to which the fetched records are eventually mapped + * @since 6.0 + */ + interface RecordFetchSpec { + + /** + * Fetches exactly one record and throws an exception if there are more entries. + * @return the one and only record. + */ + Mono one(); + + /** + * Fetches only the first record. Returns an empty holder if there are no records. + * @return the first record if any. + */ + Mono first(); + + /** + * Fetches all records. + * @return all records. + */ + Flux all(); + + } + + /** + * Contract for a runnable query that can be either run returning its result, run + * without results or be parameterized. + * + * @since 6.0 + */ + interface RunnableSpec extends BindSpec { + + /** + * Create a mapping for each record return to a specific type. + * @param targetClass the class each record should be mapped to + * @param the type of the class + * @return a mapping spec that allows specifying a mapping function + */ + MappingSpec fetchAs(Class targetClass); + + /** + * Fetch all records mapped into generic maps. + * @return a fetch specification that maps into generic maps + */ + RecordFetchSpec> fetch(); + + /** + * Execute the query and discard the results. It returns the drivers result + * summary, including various counters and other statistics. + * @return a mono containing the native summary of the query. + */ + Mono run(); + + } + + /** + * Contract for a runnable query specification which still can be bound to a specific + * database and an impersonated user. + * + * @since 6.2 + */ + interface UnboundRunnableSpec extends RunnableSpec { + + /** + * Pins the previously defined query to a specific database. A value of + * {@literal null} chooses the default database. The empty string {@literal ""} is + * not permitted. + * @param targetDatabase selected database to use. A {@literal null} value + * indicates the default database. + * @return a runnable query specification that is now bound to a given database. + */ + RunnableSpecBoundToDatabase in(String targetDatabase); + + /** + * Pins the previously defined query to an impersonated user. A value of + * {@literal null} chooses the user owning the physical connection. The empty + * string {@literal ""} is not permitted. + * @param asUser the name of the user to impersonate. A {@literal null} value + * indicates the connected user. + * @return a runnable query specification that is now bound to a given database. + */ + RunnableSpecBoundToUser asUser(String asUser); + + } + + /** + * Contract for a runnable query inside a dedicated database. + * + * @since 6.0 + */ + interface RunnableSpecBoundToDatabase extends RunnableSpec { + + RunnableSpecBoundToDatabaseAndUser asUser(String aUser); + + } + + /** + * Contract for a runnable query bound to a user to be impersonated. + * + * @since 6.2 + */ + interface RunnableSpecBoundToUser extends RunnableSpec { + + RunnableSpecBoundToDatabaseAndUser in(String aDatabase); + + } + + /** + * Combination of {@link Neo4jClient.RunnableSpecBoundToDatabase} and + * {@link Neo4jClient.RunnableSpecBoundToUser}, can't be bound any further. + * + * @since 6.2 + */ + interface RunnableSpecBoundToDatabaseAndUser extends RunnableSpec { + + } + + /** + * A contract for an ongoing delegation in the selected database. + * + * @param the type of the returned value. + * @since 6.0 + */ + interface OngoingDelegation extends RunnableDelegation { + + /** + * Runs the delegation in the given target database. + * @param targetDatabase selected database to use. A {@literal null} value + * indicates the default database. + * @return an ongoing delegation + */ + RunnableDelegation in(String targetDatabase); + + } + + /** + * A runnable delegation. + * + * @param the type that gets returned by the query + * @since 6.0 + */ + interface RunnableDelegation { + + /** + * Runs the stored callback. + * @return the optional result of the callback that has been executed with the + * given database. + */ + Mono run(); + + } + /** * A builder for {@link ReactiveNeo4jClient reactive Neo4j clients}. */ @API(status = API.Status.STABLE, since = "6.2") @SuppressWarnings("HiddenField") - class Builder { + final class Builder { final Driver driver; @@ -91,25 +344,28 @@ public interface ReactiveNeo4jClient { } /** - * Configures the database selection provider. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. During runtime, it will be - * checked if a call is made for the same database when happening in a managed transaction. - * - * @param databaseSelectionProvider The database selection provider - * @return The builder + * Configures the database selection provider. Make sure to use the same instance + * as for a possible + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. + * During runtime, it will be checked if a call is made for the same database when + * happening in a managed transaction. + * @param databaseSelectionProvider the database selection provider + * @return the builder */ - public Builder withDatabaseSelectionProvider(@Nullable ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public Builder withDatabaseSelectionProvider( + @Nullable ReactiveDatabaseSelectionProvider databaseSelectionProvider) { this.databaseSelectionProvider = databaseSelectionProvider; return this; } /** - * Configures a provider for impersonated users. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. During runtime, it will be - * checked if a call is made for the same user when happening in a managed transaction. - * - * @param impersonatedUserProvider The provider for impersonated users - * @return The builder + * Configures a provider for impersonated users. Make sure to use the same + * instance as for a possible + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. + * During runtime, it will be checked if a call is made for the same user when + * happening in a managed transaction. + * @param impersonatedUserProvider the provider for impersonated users + * @return the builder */ public Builder withUserSelectionProvider(@Nullable ReactiveUserSelectionProvider impersonatedUserProvider) { this.impersonatedUserProvider = impersonatedUserProvider; @@ -118,9 +374,9 @@ public interface ReactiveNeo4jClient { /** * Configures the set of {@link Neo4jConversions} to use. - * - * @param neo4jConversions the set of conversions to use, can be {@literal null}, in this case the default set is used. - * @return The builder + * @param neo4jConversions the set of conversions to use, can be {@literal null}, + * in this case the default set is used. + * @return the builder * @since 6.3.3 */ public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) { @@ -129,12 +385,14 @@ public interface ReactiveNeo4jClient { } /** - * Configures the {@link Neo4jBookmarkManager} to use. - * This should be the same instance as provided for the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager} - * respectively the {@link org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager}. - * - * @param bookmarkManager Neo4jBookmarkManager instance that is shared with the transaction manager. - * @return The builder + * Configures the {@link Neo4jBookmarkManager} to use. This should be the same + * instance as provided for the + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager} + * respectively the + * {@link org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager}. + * @param bookmarkManager the Neo4jBookmarkManager instance that is shared with + * the transaction manager. + * @return the builder * @since 7.1.2 */ public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) { @@ -145,238 +403,7 @@ public interface ReactiveNeo4jClient { public ReactiveNeo4jClient build() { return new DefaultReactiveNeo4jClient(this); } + } - /** - * @return A managed query runner - * @see #getQueryRunner(Mono) - * @since 6.2 - */ - default Mono getQueryRunner() { - return getQueryRunner(Mono.just(DatabaseSelection.undecided())); - } - - /** - * @return A managed query runner - * @see #getQueryRunner(Mono, Mono) - * @since 6.2 - */ - default Mono getQueryRunner(Mono databaseSelection) { - return getQueryRunner(databaseSelection, Mono.just(UserSelection.connectedUser())); - } - - /** - * Retrieves a query runner that will participate in ongoing Spring transactions (either in declarative - * (implicit via {@code @Transactional}) or in programmatically (explicit via transaction template) ones). - * This runner can be used with the Cypher-DSL for example. - * If the client cannot retrieve an ongoing Spring transaction, this runner will use auto-commit semantics. - * - * @param databaseSelection The target database. - * @param userSelection The user selection - * @return A managed query runner - * @since 6.2 - */ - Mono getQueryRunner(Mono databaseSelection, Mono userSelection); - - /** - * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether it's a match, merge, create or - * removal of things. - * - * @param cypher The cypher code that shall be executed - * @return A new CypherSpec - */ - UnboundRunnableSpec query(String cypher); - - /** - * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, - * merge, create or removal of things. The supplier can be an arbitrary Supplier that may provide a DSL for generating - * the Cypher statement. - * - * @param cypherSupplier A supplier of arbitrary Cypher code - * @return A runnable query specification. - */ - UnboundRunnableSpec query(Supplier cypherSupplier); - - /** - * Delegates interaction with the default database to the given callback. - * - * @param callback A function receiving a reactive statement runner for database interaction that can optionally - * return a publisher with none or exactly one element - * @param The type of the result being produced - * @return A single publisher containing none or exactly one element that will be produced by the callback - */ - OngoingDelegation delegateTo(Function> callback); - - /** - * Returns the assigned database selection provider. - * - * @return The database selection provider - can be null - */ - @Nullable - ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider(); - - /** - * @param The resulting type of this mapping - * @since 6.0 - */ - interface MappingSpec extends RecordFetchSpec { - - /** - * The mapping function is responsible to turn one record into one domain object. It will receive the record itself - * and in addition, the type system that the Neo4j Java-Driver used while executing the query. - * - * @param mappingFunction The mapping function used to create new domain objects - * @return A specification how to fetch one or more records. - */ - RecordFetchSpec mappedBy(BiFunction mappingFunction); - } - - /** - * @param The type to which the fetched records are eventually mapped - * @since 6.0 - */ - interface RecordFetchSpec { - - /** - * Fetches exactly one record and throws an exception if there are more entries. - * - * @return The one and only record. - */ - Mono one(); - - /** - * Fetches only the first record. Returns an empty holder if there are no records. - * - * @return The first record if any. - */ - Mono first(); - - /** - * Fetches all records. - * - * @return All records. - */ - Flux all(); - } - - /** - * Contract for a runnable query that can be either run returning its result, run without results or be - * parameterized. - * - * @since 6.0 - */ - interface RunnableSpec extends BindSpec { - - /** - * Create a mapping for each record return to a specific type. - * - * @param targetClass The class each record should be mapped to - * @param The type of the class - * @return A mapping spec that allows specifying a mapping function - */ - MappingSpec fetchAs(Class targetClass); - - /** - * Fetch all records mapped into generic maps - * - * @return A fetch specification that maps into generic maps - */ - RecordFetchSpec> fetch(); - - /** - * Execute the query and discard the results. It returns the drivers result summary, including various counters and - * other statistics. - * - * @return A mono containing the native summary of the query. - */ - Mono run(); - } - - /** - * Contract for a runnable query specification which still can be bound to a specific database and an impersonated user. - * - * @since 6.2 - */ - interface UnboundRunnableSpec extends RunnableSpec { - - /** - * Pins the previously defined query to a specific database. A value of {@literal null} chooses the default - * database. The empty string {@literal ""} is not permitted. - * - * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. - * @return A runnable query specification that is now bound to a given database. - */ - RunnableSpecBoundToDatabase in(String targetDatabase); - - /** - * Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning - * the physical connection. The empty string {@literal ""} is not permitted. - * - * @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user. - * @return A runnable query specification that is now bound to a given database. - */ - RunnableSpecBoundToUser asUser(String asUser); - } - - /** - * Contract for a runnable query inside a dedicated database. - * - * @since 6.0 - */ - interface RunnableSpecBoundToDatabase extends RunnableSpec { - - RunnableSpecBoundToDatabaseAndUser asUser(String aUser); - } - - /** - * Contract for a runnable query bound to a user to be impersonated. - * - * @since 6.2 - */ - interface RunnableSpecBoundToUser extends RunnableSpec { - - RunnableSpecBoundToDatabaseAndUser in(String aDatabase); - } - - /** - * Combination of {@link Neo4jClient.RunnableSpecBoundToDatabase} and {@link Neo4jClient.RunnableSpecBoundToUser}, can't be - * bound any further. - * - * @since 6.2 - */ - interface RunnableSpecBoundToDatabaseAndUser extends RunnableSpec { - } - - /** - * A contract for an ongoing delegation in the selected database. - * - * @param The type of the returned value. - * @since 6.0 - */ - interface OngoingDelegation extends RunnableDelegation { - - /** - * Runs the delegation in the given target database. - * - * @param targetDatabase selected database to use. A {@literal null} value indicates the default database. - * @return An ongoing delegation - */ - RunnableDelegation in(String targetDatabase); - } - - /** - * A runnable delegation. - * - * @param the type that gets returned by the query - * @since 6.0 - */ - interface RunnableDelegation { - - /** - * Runs the stored callback. - * - * @return The optional result of the callback that has been executed with the given database. - */ - Mono run(); - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java index 87146101a..b954a9501 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jOperations.java @@ -15,22 +15,23 @@ */ package org.springframework.data.neo4j.core; -import org.jspecify.annotations.Nullable; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Map; import java.util.function.BiPredicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; /** - * Specifies reactive operations one can perform on a database, based on an Domain Type. + * Specifies reactive operations one can perform on a database, based on an Domain + * Type. * * @author Michael J. Simons * @since 6.0 @@ -40,122 +41,114 @@ public interface ReactiveNeo4jOperations { /** * Counts the number of entities of a given type. - * * @param domainType the type of the entities to be counted. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ Mono count(Class domainType); /** * Counts the number of entities of a given type. - * * @param statement the Cypher {@link Statement} that returns the count. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ Mono count(Statement statement); /** * Counts the number of entities of a given type. - * * @param statement the Cypher {@link Statement} that returns the count. - * @param parameters Map of parameters. Must not be {@code null}. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @param parameters map of parameters. Must not be {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ Mono count(Statement statement, Map parameters); /** * Counts the number of entities of a given type. - * * @param cypherQuery the Cypher query that returns the count. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ Mono count(String cypherQuery); /** * Counts the number of entities of a given type. - * * @param cypherQuery the Cypher query that returns the count. - * @param parameters Map of parameters. Must not be {@code null}. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + * @param parameters map of parameters. Must not be {@code null}. + * @return the number of instances stored in the database. Guaranteed to be not + * {@code null}. */ Mono count(String cypherQuery, Map parameters); /** * Load all entities of a given type. - * * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAll(Class domainType); /** * Load all entities of a given type by executing given statement. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAll(Statement statement, Class domainType); /** * Load all entities of a given type by executing given statement with parameters. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null}. + * @param parameters map of parameters. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAll(Statement statement, Map parameters, Class domainType); /** * Load one entity of a given type by executing given statement with parameters. - * - * @param statement Cypher {@link Statement}. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. + * @param statement the Cypher {@link Statement}. Must not be {@code null}. + * @param parameters map of parameters. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Mono findOne(Statement statement, Map parameters, Class domainType); /** * Load all entities of a given type by executing given statement. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAll(String cypherQuery, Class domainType); /** * Load all entities of a given type by executing given statement with parameters. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null}. + * @param parameters map of parameters. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAll(String cypherQuery, Map parameters, Class domainType); /** * Load one entity of a given type by executing given statement with parameters. - * - * @param cypherQuery Cypher query string. Must not be {@code null}. - * @param parameters Map of parameters. Must not be {@code null}. + * @param cypherQuery the Cypher query string. Must not be {@code null}. + * @param parameters map of parameters. Must not be {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Mono findOne(String cypherQuery, Map parameters, Class domainType); /** * Load an entity from the database. - * * @param id the id of the entity to load. Must not be {@code null}. * @param domainType the type of the entity. Must not be {@code null}. * @param the type of the entity. @@ -165,27 +158,25 @@ public interface ReactiveNeo4jOperations { /** * Load all entities of a given type that are identified by the given ids. - * - * @param ids of the entities identifying the entities to load. Must not be {@code null}. + * @param ids of the entities identifying the entities to load. Must not be + * {@code null}. * @param domainType the type of the entities. Must not be {@code null}. * @param the type of the entities. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. + * @return guaranteed to be not {@code null}. */ Flux findAllById(Iterable ids, Class domainType); /** * Check if an entity for a given id exists in the database. - * * @param id the id of the entity to check. Must not be {@code null}. * @param domainType the type of the entity. Must not be {@code null}. * @param the type of the entity. - * @return If entity exists in the database, true, otherwise false. + * @return if entity exists in the database, true, otherwise false. */ Mono existsById(Object id, Class domainType); /** * Saves an instance of an entity, including all the related entities of the entity. - * * @param instance the entity to be saved. Must not be {@code null}. * @param the type of the entity. * @return the saved instance. @@ -193,16 +184,18 @@ public interface ReactiveNeo4jOperations { Mono save(T instance); /** - * Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate - * as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include - * the association property as well (meaning the predicate must return {@literal true} for that property, too). + * Saves an instance of an entity, using the provided predicate to shape the stored + * graph. One can think of the predicate as a dynamic projection. If you want to save + * or update properties of associations (aka related nodes), you must include the + * association property as well (meaning the predicate must return {@literal true} for + * that property, too). *

- * Be careful when reusing the returned instance for further persistence operations, as it will most likely not be - * fully hydrated and without using a static or dynamic projection, you will most likely cause data loss. - * - * @param instance the entity to be saved. Must not be {@code null}. - * @param includeProperty A predicate to determine the properties to save. - * @param the type of the entity. + * Be careful when reusing the returned instance for further persistence operations, + * as it will most likely not be fully hydrated and without using a static or dynamic + * projection, you will most likely cause data loss. + * @param instance the entity to be saved. Must not be {@code null}. + * @param includeProperty a predicate to determine the properties to save. + * @param the type of the entity. * @return the saved instance. * @since 6.3 */ @@ -211,9 +204,10 @@ public interface ReactiveNeo4jOperations { } /** - * Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}. - * + * Saves an instance of an entity, including the properties and relationship defined + * by the projected {@code resultType}. * @param instance the entity to be saved. Must not be {@code null}. + * @param resultType the projected type that will be returned * @param the type of the entity. * @param the type of the projection to be used during save. * @return the saved, projected instance. @@ -224,8 +218,8 @@ public interface ReactiveNeo4jOperations { } /** - * Saves several instances of an entity, including all the related entities of the entity. - * + * Saves several instances of an entity, including all the related entities of the + * entity. * @param instances the instances to be saved. Must not be {@code null}. * @param the type of the entity. * @return the saved instances. @@ -233,27 +227,31 @@ public interface ReactiveNeo4jOperations { Flux saveAll(Iterable instances); /** - * Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate - * as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include - * the association property as well (meaning the predicate must return {@literal true} for that property, too). + * Saves several instances of an entity, using the provided predicate to shape the + * stored graph. One can think of the predicate as a dynamic projection. If you want + * to save or update properties of associations (aka related nodes), you must include + * the association property as well (meaning the predicate must return {@literal true} + * for that property, too). *

- * Be careful when reusing the returned instances for further persistence operations, as they will most likely not be - * fully hydrated and without using a static or dynamic projection, you will most likely cause data loss. - * - * @param instances the instances to be saved. Must not be {@code null}. - * @param includeProperty A predicate to determine the properties to save. - * @param the type of the entity. + * Be careful when reusing the returned instances for further persistence operations, + * as they will most likely not be fully hydrated and without using a static or + * dynamic projection, you will most likely cause data loss. + * @param instances the instances to be saved. Must not be {@code null}. + * @param includeProperty a predicate to determine the properties to save. + * @param the type of the entity. * @return the saved instances. * @since 6.3 */ - default Flux saveAllAs(Iterable instances, BiPredicate includeProperty) { + default Flux saveAllAs(Iterable instances, + BiPredicate includeProperty) { throw new UnsupportedOperationException(); } /** - * Saves several instances of an entity, including the properties and relationship defined by the project {@code resultType}. - * + * Saves several instances of an entity, including the properties and relationship + * defined by the project {@code resultType}. * @param instances the instances to be saved. Must not be {@code null}. + * @param resultType the projected type that will be returned * @param the type of the entity. * @param the type of the projection to be used during save. * @return the saved, projected instance. @@ -265,51 +263,55 @@ public interface ReactiveNeo4jOperations { /** * Deletes a single entity including all entities related to that entity. - * * @param id the id of the entity to be deleted. Must not be {@code null}. * @param domainType the type of the entity * @param the type of the entity. + * @return a signal that the object has been deleted */ Mono deleteById(Object id, Class domainType); - Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue); + Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, + @Nullable Object versionValue); /** - * Deletes all entities with one of the given ids, including all entities related to that entity. - * + * Deletes all entities with one of the given ids, including all entities related to + * that entity. * @param ids the ids of the entities to be deleted. Must not be {@code null}. * @param domainType the type of the entity * @param the type of the entity. + * @return a signal that completes after all objects have been deleted */ Mono deleteAllById(Iterable ids, Class domainType); /** * Delete all entities of a given type. - * * @param domainType type of the entities to be deleted. Must not be {@code null}. + * @return a signal that completes after all objects of the given type have been + * deleted */ Mono deleteAll(Class domainType); /** - * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and - * an optional mapping function, and turns it into an executable query. - * - * @param preparedQuery prepared query that should get converted to an executable query - * @param The type of the objects returned by this query. - * @return An executable query + * Takes a prepared query, containing all the information about the cypher template to + * be used, needed parameters and an optional mapping function, and turns it into an + * executable query. + * @param preparedQuery prepared query that should get converted to an executable + * query + * @param the type of the objects returned by this query. + * @return an executable query */ Mono> toExecutableQuery(PreparedQuery preparedQuery); /** * Create an executable query based on query fragment. - * * @param domainType domain class the executable query should return - * @param queryFragmentsAndParameters fragments and parameters to construct the query from - * @param The type of the objects returned by this query. - * @return An executable query + * @param queryFragmentsAndParameters fragments and parameters to construct the query + * from + * @param the type of the objects returned by this query. + * @return an executable query */ Mono> toExecutableQuery(Class domainType, - QueryFragmentsAndParameters queryFragmentsAndParameters); + QueryFragmentsAndParameters queryFragmentsAndParameters); /** * An interface for controlling query execution in a reactive fashion. @@ -320,14 +322,19 @@ public interface ReactiveNeo4jOperations { interface ExecutableQuery { /** - * @return All results returned by this query. + * Returns all results returned by this query. + * @return all results returned by this query */ Flux getResults(); /** - * @return A single result - * @throws IncorrectResultSizeDataAccessException if there are more than one result + * Returns a single result. + * @return a single result + * @throws IncorrectResultSizeDataAccessException if there are more than one + * result */ Mono getSingleResult(); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index b37e3a2e3..79942cf11 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.neo4j.cypherdsl.core.Cypher.anyNode; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -55,6 +51,12 @@ import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuples; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -101,22 +103,23 @@ import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuple3; -import reactor.util.function.Tuples; +import static org.neo4j.cypherdsl.core.Cypher.anyNode; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import static org.neo4j.cypherdsl.core.Cypher.parameter; /** + * The Neo4j template combines various operations. All simple repositories will delegate + * to it. It provides a convenient way of dealing with mapped domain objects without + * having to define repositories for each type. + * * @author Michael J. Simons * @author Gerrit Meier * @author Philipp Tölle * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") -public final class ReactiveNeo4jTemplate implements - ReactiveNeo4jOperations, ReactiveFluentNeo4jOperations, - BeanClassLoaderAware, BeanFactoryAware { +public final class ReactiveNeo4jTemplate + implements ReactiveNeo4jOperations, ReactiveFluentNeo4jOperations, BeanClassLoaderAware, BeanFactoryAware { private static final LogAccessor log = new LogAccessor(LogFactory.getLog(ReactiveNeo4jTemplate.class)); @@ -124,12 +127,6 @@ public final class ReactiveNeo4jTemplate implements private static final String CONTEXT_RELATIONSHIP_HANDLER = "RELATIONSHIP_HANDLER"; - private final ReactiveNeo4jClient neo4jClient; - - private final Neo4jMappingContext neo4jMappingContext; - - private final CypherGenerator cypherGenerator; - private static final TransactionDefinition readOnlyTransactionDefinition = new TransactionDefinition() { @Override public boolean isReadOnly() { @@ -137,6 +134,12 @@ public final class ReactiveNeo4jTemplate implements } }; + private final ReactiveNeo4jClient neo4jClient; + + private final Neo4jMappingContext neo4jMappingContext; + + private final CypherGenerator cypherGenerator; + @Nullable private TransactionalOperator transactionalOperatorReadOnly; @@ -159,7 +162,8 @@ public final class ReactiveNeo4jTemplate implements this(neo4jClient, neo4jMappingContext, null); } - public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, @Nullable ReactiveTransactionManager transactionManager) { + public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, + @Nullable ReactiveTransactionManager transactionManager) { Assert.notNull(neo4jClient, "The Neo4jClient is required"); Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required"); @@ -167,21 +171,25 @@ public final class ReactiveNeo4jTemplate implements this.neo4jClient = neo4jClient; this.neo4jMappingContext = neo4jMappingContext; this.cypherGenerator = CypherGenerator.INSTANCE; - this.eventSupport = ReactiveEventSupport.useExistingCallbacks(neo4jMappingContext, ReactiveEntityCallbacks.create()); + this.eventSupport = ReactiveEventSupport.useExistingCallbacks(neo4jMappingContext, + ReactiveEntityCallbacks.create()); this.renderer = Renderer.getDefaultRenderer(); this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(null); setTransactionManager(transactionManager); } ProjectionFactory getProjectionFactory() { - return Objects.requireNonNull(this.projectionFactory, "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean."); + return Objects.requireNonNull(this.projectionFactory, + "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean."); } @Override public Mono count(Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Cypher.count(asterisk())).build(); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + Statement statement = this.cypherGenerator.prepareMatchOf(entityMetaData) + .returning(Cypher.count(asterisk())) + .build(); return count(statement); } @@ -193,7 +201,7 @@ public final class ReactiveNeo4jTemplate implements @Override public Mono count(Statement statement, Map parameters) { - return count(renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); + return count(this.renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); } @Override @@ -203,8 +211,10 @@ public final class ReactiveNeo4jTemplate implements @Override public Mono count(String cypherQuery, Map parameters) { - PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery) - .withParameters(parameters).build(); + PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) + .withCypherQuery(cypherQuery) + .withParameters(parameters) + .build(); return executeReadOnly(this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult)); } @@ -232,9 +242,9 @@ public final class ReactiveNeo4jTemplate implements private Flux doFindAll(Class domainType, @Nullable Class resultType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); return createExecutableQuery(domainType, resultType, QueryFragmentsAndParameters.forFindAll(entityMetaData)) - .flatMapMany(ExecutableQuery::getResults); + .flatMapMany(ExecutableQuery::getResults); } @Override @@ -246,13 +256,15 @@ public final class ReactiveNeo4jTemplate implements @Override public Flux findAll(Statement statement, Map parameters, Class domainType) { - return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters) + .flatMapMany(ExecutableQuery::getResults)); } @Override public Mono findOne(Statement statement, Map parameters, Class domainType) { - return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMap(ExecutableQuery::getSingleResult)); + return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters) + .flatMap(ExecutableQuery::getSingleResult)); } @Override @@ -262,12 +274,14 @@ public final class ReactiveNeo4jTemplate implements @Override public Flux findAll(String cypherQuery, Map parameters, Class domainType) { - return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults)); + return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters) + .flatMapMany(ExecutableQuery::getResults)); } @Override public Mono findOne(String cypherQuery, Map parameters, Class domainType) { - return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult)); + return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters) + .flatMap(ExecutableQuery::getSingleResult)); } @Override @@ -276,22 +290,26 @@ public final class ReactiveNeo4jTemplate implements } @SuppressWarnings("unchecked") - Flux doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { + Flux doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, + Class resultType, TemplateSupport.FetchType fetchType, + @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { Flux intermediaResults; if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) { intermediaResults = doFindAll(domainType, resultType); - } else { + } + else { Mono> executableQuery; if (queryFragmentsAndParameters == null) { executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(cypherQuery), - parameters == null ? Collections.emptyMap() : parameters); - } else { + (parameters != null) ? parameters : Collections.emptyMap()); + } + else { executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters); } intermediaResults = switch (fetchType) { - case ALL -> executeReadOnly(executableQuery.flatMapMany(ExecutableQuery::getResults)); + case ALL -> executeReadOnly(executableQuery.flatMapMany(ExecutableQuery::getResults)); case ONE -> executeReadOnly(executableQuery.flatMap(ExecutableQuery::getSingleResult).flux()); }; } @@ -304,18 +322,17 @@ public final class ReactiveNeo4jTemplate implements return intermediaResults.map(instance -> getProjectionFactory().createProjection(resultType, instance)); } - DtoInstantiatingConverter converter = new DtoInstantiatingConverter(resultType, neo4jMappingContext); - return (Flux) intermediaResults.map(EntityInstanceWithSource.class::cast) - .mapNotNull(converter::convert); + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(resultType, this.neo4jMappingContext); + return (Flux) intermediaResults.map(EntityInstanceWithSource.class::cast).mapNotNull(converter::convert); } @Override public Mono existsById(Object id, Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); - QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters - .forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); + QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters.forExistsById(entityMetaData, + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); Statement statement = fragmentsAndParameters.getQueryFragments().toStatement(); Map parameters = fragmentsAndParameters.getParameters(); @@ -326,28 +343,30 @@ public final class ReactiveNeo4jTemplate implements @Override public Mono findById(Object id, Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); return executeReadOnly(createExecutableQuery(domainType, null, QueryFragmentsAndParameters.forFindById(entityMetaData, - TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id))) - .flatMap(ExecutableQuery::getSingleResult)); + TemplateSupport.convertIdValues(this.neo4jMappingContext, + entityMetaData.getRequiredIdProperty(), id))) + .flatMap(ExecutableQuery::getSingleResult)); } @Override public Flux findAllById(Iterable ids, Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); return executeReadOnly(createExecutableQuery(domainType, null, - QueryFragmentsAndParameters.forFindByAllId(entityMetaData, - TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids))) - .flatMapMany(ExecutableQuery::getResults)); + QueryFragmentsAndParameters.forFindByAllId(entityMetaData, + TemplateSupport.convertIdValues(this.neo4jMappingContext, + entityMetaData.getRequiredIdProperty(), ids))) + .flatMapMany(ExecutableQuery::getResults)); } @Override public Mono> toExecutableQuery(Class domainType, - QueryFragmentsAndParameters queryFragmentsAndParameters) { + QueryFragmentsAndParameters queryFragmentsAndParameters) { return createExecutableQuery(domainType, null, queryFragmentsAndParameters); } @@ -365,7 +384,8 @@ public final class ReactiveNeo4jTemplate implements return Mono.empty(); } - return execute(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null)); + return execute(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate( + this.neo4jMappingContext, instance.getClass(), includeProperty), null)); } @Override @@ -383,29 +403,33 @@ public final class ReactiveNeo4jTemplate implements ProjectionFactory localProjectionFactory = getProjectionFactory(); ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); - Collection pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType, - localProjectionFactory, neo4jMappingContext); + Collection pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), + resultType, localProjectionFactory, this.neo4jMappingContext); Mono savingPublisher = execute(saveImpl(instance, pps, null)); if (!resultType.isInterface()) { return savingPublisher.map(savedInstance -> { @SuppressWarnings("unchecked") - R result = (R) (new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedInstance)); + R result = (R) (new DtoInstantiatingConverter(resultType, this.neo4jMappingContext) + .convertDirectly(savedInstance)); return result; }); } if (projectionInformation.isClosed()) { - return savingPublisher.map(savedInstance -> localProjectionFactory.createProjection(resultType, savedInstance)); + return savingPublisher + .map(savedInstance -> localProjectionFactory.createProjection(resultType, savedInstance)); } return savingPublisher.flatMap(savedInstance -> { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass()); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(savedInstance.getClass()); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return executeReadOnly(this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()) - .map(loadedValue -> localProjectionFactory.createProjection(resultType, loadedValue))); + return executeReadOnly(this + .findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()) + .map(loadedValue -> localProjectionFactory.createProjection(resultType, loadedValue))); }); } @@ -415,89 +439,101 @@ public final class ReactiveNeo4jTemplate implements return Flux.empty(); } - Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType)); + Class resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), + () -> "Could not find a common type element to store and then project multiple instances of type %s" + .formatted(domainType)); Collection pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType, - getProjectionFactory(), neo4jMappingContext); + getProjectionFactory(), this.neo4jMappingContext); - NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); + NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine( + this.neo4jMappingContext); Collection knownRelationshipsIds = new HashSet<>(); - EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); - return Flux.fromIterable(instances) - .concatMap(instance -> { - T domainObject = converter.convert(instance); - if (domainObject == null) { - return Mono.empty(); - } + EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, + this.neo4jMappingContext); + return Flux.fromIterable(instances).concatMap(instance -> { + T domainObject = converter.convert(instance); + if (domainObject == null) { + return Mono.empty(); + } - @SuppressWarnings("unchecked") - Mono result = execute(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds) - .map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity))); - return result; - }); + @SuppressWarnings("unchecked") + Mono result = execute(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds) + .map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, this.neo4jMappingContext) + .convertDirectly(savedEntity))); + return result; + }); } - - private Mono saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { + private Mono saveImpl(T instance, Collection includedProperties, + @Nullable NestedRelationshipProcessingStateMachine stateMachine) { return saveImpl(instance, includedProperties, stateMachine, new HashSet<>()); } @SuppressWarnings("deprecation") - private Mono saveImpl(T instance, Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds) { + private Mono saveImpl(T instance, Collection includedProperties, + @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds) { if (stateMachine != null && stateMachine.hasProcessedValue(instance)) { return Mono.just(instance); } - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(instance.getClass()); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(instance.getClass()); boolean isNewEntity = entityMetaData.isNew(instance); NestedRelationshipProcessingStateMachine finalStateMachine; if (stateMachine == null) { - finalStateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); - } else { + finalStateMachine = new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext); + } + else { finalStateMachine = stateMachine; } - return Mono.just(instance).flatMap(eventSupport::maybeCallBeforeBind) - .flatMap(entityToBeSaved -> determineDynamicLabels(entityToBeSaved, entityMetaData)).flatMap(t -> { - T entityToBeSaved = t.getT1(); + return Mono.just(instance) + .flatMap(this.eventSupport::maybeCallBeforeBind) + .flatMap(entityToBeSaved -> determineDynamicLabels(entityToBeSaved, entityMetaData)) + .flatMap(t -> { + T entityToBeSaved = t.getT1(); - DynamicLabels dynamicLabels = t.getT2(); + DynamicLabels dynamicLabels = t.getT2(); - @SuppressWarnings("unchecked") - FilteredBinderFunction binderFunction = TemplateSupport.createAndApplyPropertyFilter( - includedProperties, entityMetaData, - neo4jMappingContext.getRequiredBinderFunctionFor((Class) entityToBeSaved.getClass())); + @SuppressWarnings("unchecked") + FilteredBinderFunction binderFunction = TemplateSupport.createAndApplyPropertyFilter( + includedProperties, entityMetaData, + this.neo4jMappingContext.getRequiredBinderFunctionFor((Class) entityToBeSaved.getClass())); - boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer); - Mono idMono = this.neo4jClient.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, canUseElementId))) - .bind(entityToBeSaved) - .with(binderFunction) - .fetchAs(Entity.class) - .one() - .switchIfEmpty(Mono.defer(() -> { - if (entityMetaData.hasVersionProperty()) { - return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); - } - return Mono.empty(); - })); + boolean canUseElementId = TemplateSupport.rendererRendersElementId(this.renderer); + Mono idMono = this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, canUseElementId))) + .bind(entityToBeSaved) + .with(binderFunction) + .fetchAs(Entity.class) + .one() + .switchIfEmpty(Mono.defer(() -> { + if (entityMetaData.hasVersionProperty()) { + return Mono + .error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); + } + return Mono.empty(); + })); - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); - return idMono.doOnNext(newOrUpdatedNode -> { - var elementId = !entityMetaData.isUsingDeprecatedInternalId() && canUseElementId - ? IdentitySupport.getElementId(newOrUpdatedNode) - : newOrUpdatedNode.id(); - TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, Optional.of(newOrUpdatedNode)); - TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode); - finalStateMachine.markEntityAsProcessed(instance, elementId); - }).map(IdentitySupport::getElementId) - .flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, knownRelationshipsIds, binderFunction.filter)); - }); + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); + return idMono.doOnNext(newOrUpdatedNode -> { + var elementId = (!entityMetaData.isUsingDeprecatedInternalId() && canUseElementId) + ? IdentitySupport.getElementId(newOrUpdatedNode) : newOrUpdatedNode.id(); + TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, + Optional.of(newOrUpdatedNode)); + TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode); + finalStateMachine.markEntityAsProcessed(instance, elementId); + }) + .map(IdentitySupport::getElementId) + .flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, + finalStateMachine, knownRelationshipsIds, binderFunction.filter)); + }); } - - @SuppressWarnings("unchecked") private Mono> determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity entityMetaData) { @@ -505,21 +541,27 @@ public final class ReactiveNeo4jTemplate implements PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - ReactiveNeo4jClient.RunnableSpec runnableQuery = neo4jClient - .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty))) - .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM); + ReactiveNeo4jClient.RunnableSpec runnableQuery = this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, + propertyAccessor.getProperty(idProperty))) + .to(Constants.NAME_OF_ID) + .bind(entityMetaData.getStaticLabels()) + .to(Constants.NAME_OF_STATIC_LABELS_PARAM); if (entityMetaData.hasVersionProperty()) { runnableQuery = runnableQuery - .bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty())) - .to(Constants.NAME_OF_VERSION_PARAM); + .bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty())) + .to(Constants.NAME_OF_VERSION_PARAM); } - return runnableQuery.fetch().one().map(m -> (Collection) m.get(Constants.NAME_OF_LABELS)) - .switchIfEmpty(Mono.just(Collections.emptyList())) - .zipWith(Mono.just((Collection) propertyAccessor.getProperty(p))) - .map(t -> Tuples.of(entityToBeSaved, new DynamicLabels(entityMetaData, t.getT1(), t.getT2()))); + return runnableQuery.fetch() + .one() + .map(m -> (Collection) m.get(Constants.NAME_OF_LABELS)) + .switchIfEmpty(Mono.just(Collections.emptyList())) + .zipWith(Mono.just((Collection) propertyAccessor.getProperty(p))) + .map(t -> Tuples.of(entityToBeSaved, new DynamicLabels(entityMetaData, t.getT1(), t.getT2()))); }).orElse(Mono.just(Tuples.of(entityToBeSaved, DynamicLabels.EMPTY))); } @@ -529,7 +571,8 @@ public final class ReactiveNeo4jTemplate implements } @Override - public Flux saveAllAs(Iterable instances, BiPredicate includeProperty) { + public Flux saveAllAs(Iterable instances, + BiPredicate includeProperty) { return execute(saveAllImpl(instances, null, includeProperty)); } @@ -556,25 +599,29 @@ public final class ReactiveNeo4jTemplate implements ProjectionFactory localProjectionFactory = getProjectionFactory(); ProjectionInformation projectionInformation = localProjectionFactory.getProjectionInformation(resultType); - Collection pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType, - localProjectionFactory, neo4jMappingContext); + Collection pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, + resultType, localProjectionFactory, this.neo4jMappingContext); Flux savedInstances = execute(saveAllImpl(instances, pps, null)); if (projectionInformation.isClosed()) { return savedInstances.map(instance -> localProjectionFactory.createProjection(resultType, instance)); } - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext + .getRequiredPersistentEntity(commonElementType); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); return savedInstances.concatMap(savedInstance -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance); - return executeReadOnly(findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), commonElementType)); + return executeReadOnly( + findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), commonElementType)); }).map(instance -> localProjectionFactory.createProjection(resultType, instance)); } @SuppressWarnings("unchecked") - private Flux saveAllImpl(Iterable instances, @Nullable Collection includedProperties, @Nullable BiPredicate includeProperty) { + private Flux saveAllImpl(Iterable instances, + @Nullable Collection includedProperties, + @Nullable BiPredicate includeProperty) { Set> types = new HashSet<>(); List entities = new ArrayList<>(); @@ -590,73 +637,75 @@ public final class ReactiveNeo4jTemplate implements boolean heterogeneousCollection = types.size() > 1; Class domainClass = types.iterator().next(); - Collection pps = includeProperty == null ? - Objects.requireNonNullElseGet(includedProperties, List::of) : - TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, - includeProperty); + Collection pps = (includeProperty != null) ? TemplateSupport + .computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass, includeProperty) + : Objects.requireNonNullElseGet(includedProperties, List::of); - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainClass); if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty() - || entityMetaData.getDynamicLabelsProperty().isPresent()) { + || entityMetaData.getDynamicLabelsProperty().isPresent()) { log.debug("Saving entities using single statements."); - NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); + NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine( + this.neo4jMappingContext); return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, pps, stateMachine)); } - @SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it - Function> binderFunction = TemplateSupport.createAndApplyPropertyFilter( - pps, entityMetaData, - neo4jMappingContext.getRequiredBinderFunctionFor((Class) domainClass)); + @SuppressWarnings("unchecked") // We can safely assume here that we have a + // humongous collection with only one single type + // being either T or extending it + Function> binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, + entityMetaData, this.neo4jMappingContext.getRequiredBinderFunctionFor((Class) domainClass)); return (Flux) Flux.deferContextual((ctx) -> Flux.fromIterable(entities) - // Map all entities into a tuple - .map(e -> Tuples.of(e, entityMetaData.isNew(e))) - // Map that tuple into a tuple <, PotentiallyModified> - .zipWith(Flux.fromIterable(entities).flatMapSequential(eventSupport::maybeCallBeforeBind)) - // And for my own sanity, back into a flat Tuple3 - .map(nested -> Tuples.of(nested.getT1().getT1(), nested.getT1().getT2(), nested.getT2())) - .collectList() - .flatMapMany(entitiesToBeSaved -> Mono.defer(() -> { - // Defer the actual save statement until the previous flux completes - List> boundedEntityList = entitiesToBeSaved.stream() - .map(Tuple3::getT3) // extract PotentiallyModified - .map(binderFunction).collect(Collectors.toList()); - return neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) - .bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM) - .fetchAs(Tuple2.class) - .mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) - .all() - .collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2()); - }).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved) - .concatMap(t -> { - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3()); - Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - return processRelations(entityMetaData, propertyAccessor, t.getT2(), - ctx.get("stateMachine"), - ctx.get("knownRelIds"), - TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); - })) - )) - .contextWrite(ctx -> - ctx - .put("stateMachine", new NestedRelationshipProcessingStateMachine(neo4jMappingContext, null, null)) - .put("knownRelIds", new HashSet<>()) - ); + // Map all entities into a tuple + .map(e -> Tuples.of(e, entityMetaData.isNew(e))) + // Map that tuple into a tuple <, + // PotentiallyModified> + .zipWith(Flux.fromIterable(entities).flatMapSequential(this.eventSupport::maybeCallBeforeBind)) + // And for my own sanity, back into a flat Tuple3 + .map(nested -> Tuples.of(nested.getT1().getT1(), nested.getT1().getT2(), nested.getT2())) + .collectList() + .flatMapMany(entitiesToBeSaved -> Mono.defer(() -> { + // Defer the actual save statement until the previous flux completes + List> boundedEntityList = entitiesToBeSaved.stream() + .map(Tuple3::getT3) // extract PotentiallyModified + .map(binderFunction) + .collect(Collectors.toList()); + return this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(boundedEntityList) + .to(Constants.NAME_OF_ENTITY_LIST_PARAM) + .fetchAs(Tuple2.class) + .mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), + TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) + .all() + .collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2()); + }).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved).concatMap(t -> { + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3()); + Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); + return processRelations(entityMetaData, propertyAccessor, t.getT2(), ctx.get("stateMachine"), + ctx.get("knownRelIds"), TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); + })))) + .contextWrite(ctx -> ctx + .put("stateMachine", new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext, null, null)) + .put("knownRelIds", new HashSet<>())); } @Override public Mono deleteAllById(Iterable ids, Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); String nameOfParameter = "ids"; Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter)); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - return execute(Mono.defer(() -> - this.neo4jClient.query(() -> renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)) - .to(nameOfParameter).run().then())); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData, condition); + return execute(Mono.defer(() -> this.neo4jClient.query(() -> this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), + ids)) + .to(nameOfParameter) + .run() + .then())); } @Override @@ -665,51 +714,60 @@ public final class ReactiveNeo4jTemplate implements Assert.notNull(id, "The given id must not be null"); String nameOfParameter = "id"; - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); - return execute(Mono.defer(() -> - this.neo4jClient.query(() -> renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)) - .to(nameOfParameter).run().then())); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData, condition); + return execute(Mono.defer(() -> this.neo4jClient.query(() -> this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)) + .to(nameOfParameter) + .run() + .then())); } @Override - public Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue) { + public Mono deleteByIdWithVersion(Object id, Class domainType, Neo4jPersistentProperty versionProperty, + @Nullable Object versionValue) { String nameOfParameter = "id"; - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter)) - .and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()) - .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) - .or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull())); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + Condition condition = entityMetaData.getIdExpression() + .isEqualTo(parameter(nameOfParameter)) + .and(Cypher + .property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()) + .isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)) + .or(Cypher + .property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), + versionProperty.getPropertyName()) + .isNull())); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition) - .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build(); + Statement statement = this.cypherGenerator.prepareMatchOf(entityMetaData, condition) + .returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)) + .build(); Map parameters = new HashMap<>(); - parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); + parameters.put(nameOfParameter, + TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)); parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue); - return execute(Mono.defer(() -> - this.neo4jClient.query(() -> renderer.render(statement)) - .bindAll(parameters) - .fetch().one().switchIfEmpty(Mono.defer(() -> { - if (entityMetaData.hasVersionProperty()) { - return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); - } - return Mono.empty(); - }))) - .then(deleteById(id, domainType))); + return execute(Mono.defer(() -> this.neo4jClient.query(() -> this.renderer.render(statement)) + .bindAll(parameters) + .fetch() + .one() + .switchIfEmpty(Mono.defer(() -> { + if (entityMetaData.hasVersionProperty()) { + return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); + } + return Mono.empty(); + }))).then(deleteById(id, domainType))); } @Override public Mono deleteAll(Class domainType) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); - Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData); - return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)).run().then())); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); + Statement statement = this.cypherGenerator.prepareDeleteOf(entityMetaData); + return execute(Mono.defer(() -> this.neo4jClient.query(() -> this.renderer.render(statement)).run().then())); } private Mono> createExecutableQuery(Class domainType, Statement statement) { @@ -720,197 +778,221 @@ public final class ReactiveNeo4jTemplate implements return createExecutableQuery(domainType, null, cypherQuery, Collections.emptyMap()); } - private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, Statement statement, - Map parameters) { + private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, + Statement statement, Map parameters) { - return createExecutableQuery(domainType, resultType, renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters)); + return createExecutableQuery(domainType, resultType, this.renderer.render(statement), + TemplateSupport.mergeParameters(statement, parameters)); } - private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, String cypherQuery, - Map parameters) { + private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, + String cypherQuery, Map parameters) { Supplier> mappingFunction = TemplateSupport - .getAndDecorateMappingFunction(neo4jMappingContext, domainType, resultType); - PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType).withCypherQuery(cypherQuery) - .withParameters(parameters) - .usingMappingFunction(mappingFunction).build(); + .getAndDecorateMappingFunction(this.neo4jMappingContext, domainType, resultType); + PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType) + .withCypherQuery(cypherQuery) + .withParameters(parameters) + .usingMappingFunction(mappingFunction) + .build(); return this.toExecutableQuery(preparedQuery); } - private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, QueryFragmentsAndParameters queryFragmentsAndParameters) { + private Mono> createExecutableQuery(Class domainType, @Nullable Class resultType, + QueryFragmentsAndParameters queryFragmentsAndParameters) { - Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType); QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments(); - boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField); + boolean containsPossibleCircles = entityMetaData != null + && entityMetaData.containsPossibleCircles(queryFragments::includeField); if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) { - return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters()) - .flatMap(finalQueryAndParameters -> { - var statement = finalQueryAndParameters.toStatement(entityMetaData); - return createExecutableQuery(domainType, resultType, renderer.render(statement), - statement.getCatalog().getParameters()); - }); + return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, + queryFragmentsAndParameters.getParameters()) + .flatMap(finalQueryAndParameters -> { + var statement = finalQueryAndParameters.toStatement(entityMetaData); + return createExecutableQuery(domainType, resultType, this.renderer.render(statement), + statement.getCatalog().getParameters()); + }); } - return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters()); + return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), + queryFragmentsAndParameters.getParameters()); } - @SuppressWarnings({"unchecked"}) - private Mono createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, - QueryFragments queryFragments, Map parameters) { + @SuppressWarnings({ "unchecked" }) + private Mono createNodesAndRelationshipsByIdStatementProvider( + Neo4jPersistentEntity entityMetaData, QueryFragments queryFragments, Map parameters) { - return Mono.deferContextual(ctx -> { - Class rootClass = entityMetaData.getUnderlyingClass(); + return Mono.deferContextual(ctx -> { + Class rootClass = entityMetaData.getUnderlyingClass(); - Set rootNodeIds = ctx.get("rootNodes"); - Map> relationshipsToRelatedNodeIds = ctx.get("relationshipsToRelatedNodeIds"); - return Flux.fromIterable(entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField)) - .concatMap(relationshipDescription -> { + Set rootNodeIds = ctx.get("rootNodes"); + Map> relationshipsToRelatedNodeIds = ctx.get("relationshipsToRelatedNodeIds"); + return Flux.fromIterable(entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField)) + .concatMap(relationshipDescription -> { - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, relationshipDescription, - queryFragments.getMatchOn(), queryFragments.getCondition()) - .returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build(); + Statement statement = this.cypherGenerator + .prepareMatchOf(entityMetaData, relationshipDescription, queryFragments.getMatchOn(), + queryFragments.getCondition()) + .returning(this.cypherGenerator.createReturnStatementForMatch(entityMetaData)) + .build(); - Map usedParameters = new HashMap<>(parameters); - usedParameters.putAll(statement.getCatalog().getParameters()); - return neo4jClient.query(renderer.render(statement)) - .bindAll(usedParameters) - .fetchAs(Tuple2.class) - .mappedBy((t, r) -> { - Collection rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(TemplateSupport::convertIdOrElementIdToString); - rootNodeIds.addAll(rootIds); - Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString); - Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString); - return Tuples.of(newRelationshipIds, newRelatedNodeIds); - }) - .one() - .map((t) -> (Tuple2, Collection>) t) - .expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty())); + Map usedParameters = new HashMap<>(parameters); + usedParameters.putAll(statement.getCatalog().getParameters()); + return this.neo4jClient.query(this.renderer.render(statement)) + .bindAll(usedParameters) + .fetchAs(Tuple2.class) + .mappedBy((t, r) -> { + Collection rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) + .asList(TemplateSupport::convertIdOrElementIdToString); + rootNodeIds.addAll(rootIds); + Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS) + .asList(TemplateSupport::convertIdOrElementIdToString); + Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES) + .asList(TemplateSupport::convertIdOrElementIdToString); + return Tuples.of(newRelationshipIds, newRelatedNodeIds); }) - .then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipsToRelatedNodeIds.keySet(), relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), queryFragments, elementIdOrIdFunction))); - }) - .contextWrite(ctx -> ctx - .put("rootNodes", ConcurrentHashMap.newKeySet()) - .put("relationshipsToRelatedNodeIds", new ConcurrentHashMap<>())); + .one() + .map((t) -> (Tuple2, Collection>) t) + .expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, + PropertyPathWalkStep.empty())); + }) + .then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, + relationshipsToRelatedNodeIds.keySet(), + relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), + queryFragments, this.elementIdOrIdFunction))); + }) + .contextWrite(ctx -> ctx.put("rootNodes", ConcurrentHashMap.newKeySet()) + .put("relationshipsToRelatedNodeIds", new ConcurrentHashMap<>())); } @SuppressWarnings("unchecked") private Flux, Collection>> iterateNextLevel(Collection relatedNodeIds, - RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments, - Class rootClass, PropertyPathWalkStep currentPathStep) { + RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments, Class rootClass, + PropertyPathWalkStep currentPathStep) { NodeDescription target = sourceRelationshipDescription.getTarget(); @SuppressWarnings("unchecked") - String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName(); + String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse() + .getFieldName(); PropertyPathWalkStep nextPathStep; if (sourceRelationshipDescription.hasRelationshipProperties()) { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription.getRequiredRelationshipPropertiesEntity(); - nextPathStep = currentPathStep.with(fieldName + "." + Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())).getFieldName()); - } else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) sourceRelationshipDescription + .getRequiredRelationshipPropertiesEntity(); + nextPathStep = currentPathStep.with(fieldName + "." + + Objects + .requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), + () -> "Could not get target node property on %s" + .formatted(relationshipPropertiesEntity.getType())) + .getFieldName()); + } + else { nextPathStep = currentPathStep.with(fieldName); } - return Flux.fromIterable(target - .getRelationshipsInHierarchy( - relaxedPropertyPath -> { - PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path); - prepend = PropertyFilter.RelaxedPropertyPath.withRootType(rootClass).append(prepend.toDotPath()); - return queryFragments.includeField(prepend); - } - )) - .concatMap(relDe -> { - Node node = anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(target)); + return Flux.fromIterable(target.getRelationshipsInHierarchy(relaxedPropertyPath -> { + PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path); + prepend = PropertyFilter.RelaxedPropertyPath.withRootType(rootClass).append(prepend.toDotPath()); + return queryFragments.includeField(prepend); + })).concatMap(relDe -> { + Node node = anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(target)); - Statement statement = cypherGenerator - .prepareMatchOf(target, relDe, null, - elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_ID))) - .returning(cypherGenerator.createGenericReturnStatement()).build(); + Statement statement = this.cypherGenerator + .prepareMatchOf(target, relDe, null, + this.elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_ID))) + .returning(this.cypherGenerator.createGenericReturnStatement()) + .build(); - return neo4jClient.query(renderer.render(statement)) - .bindAll(Collections.singletonMap(Constants.NAME_OF_ID, TemplateSupport.convertToLongIdOrStringElementId(relatedNodeIds))) - .fetchAs(Tuple2.class) - .mappedBy((t, r) -> { - Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString); - Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString); + return this.neo4jClient.query(this.renderer.render(statement)) + .bindAll(Collections.singletonMap(Constants.NAME_OF_ID, + TemplateSupport.convertToLongIdOrStringElementId(relatedNodeIds))) + .fetchAs(Tuple2.class) + .mappedBy((t, r) -> { + Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS) + .asList(TemplateSupport::convertIdOrElementIdToString); + Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES) + .asList(TemplateSupport::convertIdOrElementIdToString); - return Tuples.of(newRelationshipIds, newRelatedNodeIds); - }) - .one() - .map((t) -> (Tuple2, Collection>) t) - .expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object)); - }); + return Tuples.of(newRelationshipIds, newRelatedNodeIds); + }) + .one() + .map((t) -> (Tuple2, Collection>) t) + .expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object)); + }); } - private Function, Collection>, - Publisher, Collection>>> iterateAndMapNextLevel( - RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class rootClass, PropertyPathWalkStep currentPathStep) { + private Function, Collection>, Publisher, Collection>>> iterateAndMapNextLevel( + RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class rootClass, + PropertyPathWalkStep currentPathStep) { - return newRelationshipAndRelatedNodeIds -> - Flux.deferContextual(ctx -> { - Map> relationshipsToRelatedNodeIds = ctx.get("relationshipsToRelatedNodeIds"); - Map> relatedNodesVisited = new HashMap<>(relationshipsToRelatedNodeIds); + return newRelationshipAndRelatedNodeIds -> Flux.deferContextual(ctx -> { + Map> relationshipsToRelatedNodeIds = ctx.get("relationshipsToRelatedNodeIds"); + Map> relatedNodesVisited = new HashMap<>(relationshipsToRelatedNodeIds); - Collection newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1(); + Collection newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1(); - Collection newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2(); - Set relatedIds = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size()); - relatedIds.addAll(newRelatedNodeIds); + Collection newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2(); + Set relatedIds = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size()); + relatedIds.addAll(newRelatedNodeIds); - for (String newRelationshipId : newRelationshipIds) { - relatedNodesVisited.put(newRelationshipId, relatedIds); - Set knownRelatedNodesBefore = relationshipsToRelatedNodeIds.get(newRelationshipId); - if (knownRelatedNodesBefore != null) { - Set mergedKnownRelatedNodes = new HashSet<>(knownRelatedNodesBefore); - // there are already existing nodes in there for this relationship - mergedKnownRelatedNodes.addAll(relatedIds); - relatedNodesVisited.put(newRelationshipId, mergedKnownRelatedNodes); - relatedIds.removeAll(knownRelatedNodesBefore); - } + for (String newRelationshipId : newRelationshipIds) { + relatedNodesVisited.put(newRelationshipId, relatedIds); + Set knownRelatedNodesBefore = relationshipsToRelatedNodeIds.get(newRelationshipId); + if (knownRelatedNodesBefore != null) { + Set mergedKnownRelatedNodes = new HashSet<>(knownRelatedNodesBefore); + // there are already existing nodes in there for this relationship + mergedKnownRelatedNodes.addAll(relatedIds); + relatedNodesVisited.put(newRelationshipId, mergedKnownRelatedNodes); + relatedIds.removeAll(knownRelatedNodesBefore); } - relationshipsToRelatedNodeIds.putAll(relatedNodesVisited); + } + relationshipsToRelatedNodeIds.putAll(relatedNodesVisited); - if (relatedIds.isEmpty()) { - return Mono.empty(); - } + if (relatedIds.isEmpty()) { + return Mono.empty(); + } - return iterateNextLevel(newRelatedNodeIds, relationshipDescription, queryFragments, rootClass, currentPathStep); - }); + return iterateNextLevel(newRelatedNodeIds, relationshipDescription, queryFragments, rootClass, + currentPathStep); + }); } /** * Starts of processing of the relationships. - * - * @param neo4jPersistentEntity The description of the instance to save - * @param parentPropertyAccessor The property accessor of the parent, to modify the relationships - * @param isParentObjectNew A flag if the parent was new - * @param stateMachine Initial state of entity processing - * @param includeProperty A predicate telling to include a relationship property or not - * @param The type of the object being initially processed - * @return A mono representing the whole stream of save operations, eventually containing the owner of the relations being processed + * @param neo4jPersistentEntity the description of the instance to save + * @param parentPropertyAccessor the property accessor of the parent, to modify the + * relationships + * @param isParentObjectNew a flag if the parent was new + * @param stateMachine initial state of entity processing + * @param knownRelationshipsIds a collection of ids of relationships already known / + * visited + * @param includeProperty a predicate telling to include a relationship property or + * not + * @param the type of the object being initially processed + * @return a mono representing the whole stream of save operations, eventually + * containing the owner of the relations being processed */ - private Mono processRelations( - Neo4jPersistentEntity neo4jPersistentEntity, - PersistentPropertyAccessor parentPropertyAccessor, - boolean isParentObjectNew, - NestedRelationshipProcessingStateMachine stateMachine, - Collection knownRelationshipsIds, - PropertyFilter includeProperty - ) { + private Mono processRelations(Neo4jPersistentEntity neo4jPersistentEntity, + PersistentPropertyAccessor parentPropertyAccessor, boolean isParentObjectNew, + NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds, + PropertyFilter includeProperty) { - PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass()); - return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, - stateMachine, knownRelationshipsIds, includeProperty, startingPropertyPath); + PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath + .withRootType(neo4jPersistentEntity.getUnderlyingClass()); + return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, stateMachine, + knownRelationshipsIds, includeProperty, startingPropertyPath); } @SuppressWarnings("deprecation") - private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity, PersistentPropertyAccessor parentPropertyAccessor, - boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, - Collection knownRelationshipsIds, - PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) { + private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity, + PersistentPropertyAccessor parentPropertyAccessor, boolean isParentObjectNew, + NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds, + PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) { Object fromId = parentPropertyAccessor.getProperty(sourceEntity.getRequiredIdProperty()); List> relationshipDeleteMonos = new ArrayList<>(); @@ -919,7 +1001,8 @@ public final class ReactiveNeo4jTemplate implements AssociationHandlerSupport.of(sourceEntity).doWithAssociations(association -> { // create context to bundle parameters - NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, parentPropertyAccessor, sourceEntity); + NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, + parentPropertyAccessor, sourceEntity); if (relationshipContext.isReadOnly()) { return; } @@ -930,7 +1013,8 @@ public final class ReactiveNeo4jTemplate implements RelationshipDescription relationshipDescription = relationshipContext.getRelationship(); - PropertyFilter.RelaxedPropertyPath currentPropertyPath = previousPath.append(relationshipDescription.getFieldName()); + PropertyFilter.RelaxedPropertyPath currentPropertyPath = previousPath + .append(relationshipDescription.getFieldName()); if (!includeProperty.isNotFiltering() && !includeProperty.contains(currentPropertyPath)) { return; @@ -938,48 +1022,58 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentProperty idProperty; if (!relationshipDescription.hasInternalIdProperty()) { idProperty = null; - } else { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); - idProperty = relationshipPropertiesEntity == null ? null : relationshipPropertiesEntity.getIdProperty(); + } + else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription + .getRelationshipPropertiesEntity(); + idProperty = (relationshipPropertiesEntity != null) ? relationshipPropertiesEntity.getIdProperty() + : null; } // break recursive procession and deletion of previously created relationships ProcessState processState = stateMachine.getStateOf(fromId, relationshipDescription, relatedValuesToStore); - if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS || processState == ProcessState.PROCESSED_BOTH) { + if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS + || processState == ProcessState.PROCESSED_BOTH) { return; } - // Remove all relationships before creating all new if the entity is not new and the relationship + // Remove all relationships before creating all new if the entity is not new + // and the relationship // has not been processed before. - // This avoids the usage of cache but might have significant impact on overall performance - boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer); + // This avoids the usage of cache but might have significant impact on overall + // performance + boolean canUseElementId = TemplateSupport.rendererRendersElementId(this.renderer); if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) { if (idProperty != null) { for (Object relatedValueToStore : relatedValuesToStore) { - //noinspection ConstantValue + // noinspection ConstantValue if (relatedValueToStore == null) { continue; } - Object id = Objects.requireNonNull(relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore)) - .getProperty(idProperty); + Object id = Objects + .requireNonNull( + relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)) + .getProperty(idProperty); if (id != null) { knownRelationshipsIds.add(id); } } } - Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription, canUseElementId); + Statement relationshipRemoveQuery = this.cypherGenerator.prepareDeleteOf(sourceEntity, + relationshipDescription, canUseElementId); - relationshipDeleteMonos.add( - neo4jClient.query(renderer.render(relationshipRemoveQuery)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(knownRelationshipsIds) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // - .run().checkpoint("delete relationships").then()); + relationshipDeleteMonos.add(this.neo4jClient.query(this.renderer.render(relationshipRemoveQuery)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), + fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(knownRelationshipsIds) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // + .run() + .checkpoint("delete relationships") + .then()); } // nothing to do because there is nothing to map @@ -989,229 +1083,274 @@ public final class ReactiveNeo4jTemplate implements Neo4jPersistentProperty relationshipProperty = association.getInverse(); stateMachine.markRelationshipAsProcessed(fromId, relationshipDescription); - Flux relationshipCreation = Flux.fromIterable(relatedValuesToStore).concatMap(relatedValueToStore -> { + Flux relationshipCreation = Flux.fromIterable(relatedValuesToStore) + .concatMap(relatedValueToStore -> { - Object relatedObjectBeforeCallbacksApplied = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore); - Neo4jPersistentEntity targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass()); - boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied); + Object relatedObjectBeforeCallbacksApplied = relationshipContext + .identifyAndExtractRelationshipTargetNode(relatedValueToStore); + Neo4jPersistentEntity targetEntity = this.neo4jMappingContext + .getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass()); + boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied); - return Mono.deferContextual(ctx -> + return Mono.deferContextual(ctx -> - (stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied) - ? Mono.just(stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied)) - : eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied)) + (stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied) + ? Mono.just(stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied)) + : this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied)) - .flatMap(newRelatedObject -> { + .flatMap(newRelatedObject -> { - Mono, AtomicReference>> queryOrSave; - if (stateMachine.hasProcessedValue(relatedValueToStore)) { - AtomicReference relatedInternalId = new AtomicReference<>(); - Object possibleValue = stateMachine.getObjectId(relatedValueToStore); - if (possibleValue != null) { - relatedInternalId.set(possibleValue); - } - queryOrSave = Mono.just(Tuples.of(relatedInternalId, new AtomicReference<>())); - } else { - Mono savedEntity; - if (isNewEntity || relationshipDescription.cascadeUpdates()) { - savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath); - } else { - var targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); - var requiredIdProperty = targetEntity.getRequiredIdProperty(); - savedEntity = loadRelatedNode(targetEntity, targetPropertyAccessor.getProperty(requiredIdProperty)); - } - - queryOrSave = savedEntity - .map(entity -> Tuples.of(new AtomicReference<>((Object) (TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? entity.elementId() : entity.id())), new AtomicReference<>(entity))) - .doOnNext(t -> { - var relatedInternalId = Objects.requireNonNull(t.getT1().get(), "Related internal id is null"); - stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId); - if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { - Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity(); - stateMachine.markAsAliased(entity, relatedInternalId); - } - }); + Mono, AtomicReference>> queryOrSave; + if (stateMachine.hasProcessedValue(relatedValueToStore)) { + AtomicReference relatedInternalId = new AtomicReference<>(); + Object possibleValue = stateMachine.getObjectId(relatedValueToStore); + if (possibleValue != null) { + relatedInternalId.set(possibleValue); + } + queryOrSave = Mono.just(Tuples.of(relatedInternalId, new AtomicReference<>())); + } + else { + Mono savedEntity; + if (isNewEntity || relationshipDescription.cascadeUpdates()) { + savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, + currentPropertyPath); + } + else { + var targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); + var requiredIdProperty = targetEntity.getRequiredIdProperty(); + savedEntity = loadRelatedNode(targetEntity, + targetPropertyAccessor.getProperty(requiredIdProperty)); } - return queryOrSave.flatMap(idAndEntity -> { - Object relatedInternalId = idAndEntity.getT1().get(); - Entity savedEntity = idAndEntity.getT2().get(); - Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty(); - PersistentPropertyAccessor targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject); - Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty); - //noinspection OptionalOfNullableMisuse - relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId); - //noinspection ConstantValue - if (savedEntity != null) { - TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity); + queryOrSave = savedEntity.map(entity -> Tuples.of( + new AtomicReference<>( + (Object) (TemplateSupport.rendererCanUseElementIdIfPresent(this.renderer, + targetEntity) ? entity.elementId() : entity.id())), + new AtomicReference<>(entity))) + .doOnNext(t -> { + var relatedInternalId = Objects.requireNonNull(t.getT1().get(), + "Related internal id is null"); + stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId); + if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { + Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore) + .getRelatedEntity(); + stateMachine.markAsAliased(entity, relatedInternalId); } - stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean()); - stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, - relationshipDescription.getRelationshipObverse()); - - PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); - Object idValue = (idProperty != null && relationshipPropertiesPropertyAccessor != null) - ? relationshipPropertiesPropertyAccessor.getProperty(idProperty) - : null; - - boolean isNewRelationship = idValue == null; - CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship( - sourceEntity, relationshipDescription, relatedValueToStore, isNewRelationship, canUseElementId); - - Map properties = new HashMap<>(); - properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); - properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); - properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); - var update = true; - if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties() && fromId != null) { - var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); - var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); - if (hasProcessedRelationshipEntity) { - stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, relatedInternalId, relationshipContext, relatedValueToStore, idProperty); - update = false; - } else { - stateMachine.storeProcessRelationshipEntity(hlp, parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); - } - } - List rows = new ArrayList<>(); - rows.add(properties); - statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, rows); - // in case of no properties the bind will just return an empty map - if (update) { - return neo4jClient - .query(renderer.render(statementHolder.getStatement())) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(relatedInternalId) // - .to(Constants.TO_ID_PARAMETER_NAME) // - .bind(idValue) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // - .bindAll(statementHolder.getProperties()) - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .one() - .flatMap(relationshipInternalId -> { - if (idProperty != null && isNewRelationship && relationshipPropertiesPropertyAccessor != null) { - relationshipPropertiesPropertyAccessor - .setProperty(idProperty, relationshipInternalId); - knownRelationshipsIds.add(relationshipInternalId); - } - - Mono nestedRelationshipsSignal = null; - if (processState != ProcessState.PROCESSED_ALL_VALUES) { - nestedRelationshipsSignal = processNestedRelations(targetEntity, targetPropertyAccessor, targetEntity.isNew(newRelatedObject), stateMachine, knownRelationshipsIds, includeProperty, currentPropertyPath); - } - - Mono getRelationshipOrRelationshipPropertiesObject = Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( - neo4jMappingContext, - relationshipDescription.hasRelationshipProperties(), - relationshipProperty.isDynamicAssociation(), - relatedValueToStore, - targetPropertyAccessor)); - return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject : - nestedRelationshipsSignal.then(getRelationshipOrRelationshipPropertiesObject); - }); - } - return Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( - neo4jMappingContext, - relationshipDescription.hasRelationshipProperties(), - relationshipProperty.isDynamicAssociation(), - relatedValueToStore, - targetPropertyAccessor)); - }) - .doOnNext(potentiallyRecreatedRelatedObject -> { - RelationshipHandler handler = ctx.get(CONTEXT_RELATIONSHIP_HANDLER); - handler.handle(relatedValueToStore, relatedObjectBeforeCallbacksApplied, potentiallyRecreatedRelatedObject); }); - }) - .then(Mono.fromSupplier(() -> ctx.get(CONTEXT_RELATIONSHIP_HANDLER)))); + } - }) - .contextWrite(ctx -> { - RelationshipHandler relationshipHandler = RelationshipHandler.forProperty(relationshipProperty, rawValue); - return ctx.put(CONTEXT_RELATIONSHIP_HANDLER, relationshipHandler); - }); + return queryOrSave.flatMap(idAndEntity -> { + Object relatedInternalId = idAndEntity.getT1().get(); + Entity savedEntity = idAndEntity.getT2().get(); + Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty(); + PersistentPropertyAccessor targetPropertyAccessor = targetEntity + .getPropertyAccessor(newRelatedObject); + Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty); + // noinspection OptionalOfNullableMisuse + relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, + targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId); + // noinspection ConstantValue + if (savedEntity != null) { + TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, + savedEntity); + } + stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, + targetPropertyAccessor.getBean()); + stateMachine.markRelationshipAsProcessed( + (possibleInternalLongId != null) ? possibleInternalLongId : relatedInternalId, + relationshipDescription.getRelationshipObverse()); + + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore); + Object idValue = (idProperty != null && relationshipPropertiesPropertyAccessor != null) + ? relationshipPropertiesPropertyAccessor.getProperty(idProperty) : null; + + boolean isNewRelationship = idValue == null; + CreateRelationshipStatementHolder statementHolder = this.neo4jMappingContext + .createStatementForSingleRelationship(sourceEntity, relationshipDescription, + relatedValueToStore, isNewRelationship, canUseElementId); + + Map properties = new HashMap<>(); + properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues( + this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)); + properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); + properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); + var update = true; + if (!relationshipDescription.isDynamic() + && relationshipDescription.hasRelationshipProperties() && fromId != null) { + var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); + var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity( + parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), + relationshipContext.getRelationship()); + if (hasProcessedRelationshipEntity) { + stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, + fromId, relatedInternalId, relationshipContext, relatedValueToStore, + idProperty); + update = false; + } + else { + stateMachine.storeProcessRelationshipEntity(hlp, parentPropertyAccessor.getBean(), + hlp.getRelatedEntity(), relationshipContext.getRelationship()); + } + } + List rows = new ArrayList<>(); + rows.add(properties); + statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, + rows); + // in case of no properties the bind will just return an empty + // map + if (update) { + return this.neo4jClient.query(this.renderer.render(statementHolder.getStatement())) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, + sourceEntity.getRequiredIdProperty(), fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(relatedInternalId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .bind(idValue) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // + .bindAll(statementHolder.getProperties()) + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one() + .flatMap(relationshipInternalId -> { + if (idProperty != null && isNewRelationship + && relationshipPropertiesPropertyAccessor != null) { + relationshipPropertiesPropertyAccessor.setProperty(idProperty, + relationshipInternalId); + knownRelationshipsIds.add(relationshipInternalId); + } + + Mono nestedRelationshipsSignal = null; + if (processState != ProcessState.PROCESSED_ALL_VALUES) { + nestedRelationshipsSignal = processNestedRelations(targetEntity, + targetPropertyAccessor, targetEntity.isNew(newRelatedObject), + stateMachine, knownRelationshipsIds, includeProperty, + currentPropertyPath); + } + + Mono getRelationshipOrRelationshipPropertiesObject = Mono + .fromSupplier(() -> MappingSupport + .getRelationshipOrRelationshipPropertiesObject(this.neo4jMappingContext, + relationshipDescription.hasRelationshipProperties(), + relationshipProperty.isDynamicAssociation(), + relatedValueToStore, targetPropertyAccessor)); + return (nestedRelationshipsSignal != null) + ? nestedRelationshipsSignal + .then(getRelationshipOrRelationshipPropertiesObject) + : getRelationshipOrRelationshipPropertiesObject; + }); + } + return Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( + this.neo4jMappingContext, relationshipDescription.hasRelationshipProperties(), + relationshipProperty.isDynamicAssociation(), relatedValueToStore, + targetPropertyAccessor)); + }).doOnNext(potentiallyRecreatedRelatedObject -> { + RelationshipHandler handler = ctx.get(CONTEXT_RELATIONSHIP_HANDLER); + handler.handle(relatedValueToStore, relatedObjectBeforeCallbacksApplied, + potentiallyRecreatedRelatedObject); + }); + }) + .then(Mono.fromSupplier(() -> ctx.get(CONTEXT_RELATIONSHIP_HANDLER)))); + + }) + .contextWrite(ctx -> { + RelationshipHandler relationshipHandler = RelationshipHandler.forProperty(relationshipProperty, + rawValue); + return ctx.put(CONTEXT_RELATIONSHIP_HANDLER, relationshipHandler); + }); relationshipCreationCreations.add(relationshipCreation); }); @SuppressWarnings("unchecked") Mono deleteAndThanCreateANew = (Mono) Flux.concat(relationshipDeleteMonos) - .thenMany(Flux.concat(relationshipCreationCreations)) - .doOnNext(objects -> objects.applyFinalResultToOwner(parentPropertyAccessor)) - .checkpoint() - .then(stateMachine.updateRelationshipIdsReactive(this::getRelationshipId)) - .then(Mono.fromSupplier(parentPropertyAccessor::getBean)); + .thenMany(Flux.concat(relationshipCreationCreations)) + .doOnNext(objects -> objects.applyFinalResultToOwner(parentPropertyAccessor)) + .checkpoint() + .then(stateMachine.updateRelationshipIdsReactive(this::getRelationshipId)) + .then(Mono.fromSupplier(parentPropertyAccessor::getBean)); return deleteAndThanCreateANew; } - private Mono getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + private Mono getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, + Object fromId, Object toId) { - return neo4jClient.query(renderer.render(statement)) - .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // - .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(toId) // - .to(Constants.TO_ID_PARAMETER_NAME) // - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .one(); + return this.neo4jClient.query(this.renderer.render(statement)) + .bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(toId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one(); } - // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} + // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, + // PropertyFilter, PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id private Mono loadRelatedNode(NodeDescription targetNodeDescription, @Nullable Object relatedInternalId) { var targetPersistentEntity = (Neo4jPersistentEntity) targetNodeDescription; - var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); + var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, + TemplateSupport.convertIdValues(this.neo4jMappingContext, + targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue(); - return neo4jClient - .query(() -> renderer.render( - cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), - queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build())) - .bindAll(queryFragmentsAndParameters.getParameters()) - .fetchAs(Entity.class).mappedBy((t, r) -> r.get(nodeName).asNode()) - .one(); + return this.neo4jClient + .query(() -> this.renderer + .render(this.cypherGenerator + .prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), + queryFragmentsAndParameters.getQueryFragments().getCondition()) + .returning(nodeName) + .build())) + .bindAll(queryFragmentsAndParameters.getParameters()) + .fetchAs(Entity.class) + .mappedBy((t, r) -> r.get(nodeName).asNode()) + .one(); } - private Mono saveRelatedNode(Object relatedNode, Neo4jPersistentEntity targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) { + private Mono saveRelatedNode(Object relatedNode, Neo4jPersistentEntity targetNodeDescription, + PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) { - return determineDynamicLabels(relatedNode, targetNodeDescription) - .flatMap(t -> { - Object entity = t.getT1(); - @SuppressWarnings("rawtypes") - Class entityType = entity.getClass(); - DynamicLabels dynamicLabels = t.getT2(); - @SuppressWarnings("unchecked") - Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(entityType); - String idPropertyName = targetNodeDescription.getRequiredIdProperty().getPropertyName(); - IdDescription idDescription = targetNodeDescription.getIdDescription(); - boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); - binderFunction = binderFunction.andThen(tree -> { - @SuppressWarnings("unchecked") - Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); + return determineDynamicLabels(relatedNode, targetNodeDescription).flatMap(t -> { + Object entity = t.getT1(); + @SuppressWarnings("rawtypes") + Class entityType = entity.getClass(); + DynamicLabels dynamicLabels = t.getT2(); + @SuppressWarnings("unchecked") + Function> binderFunction = this.neo4jMappingContext + .getRequiredBinderFunctionFor(entityType); + String idPropertyName = targetNodeDescription.getRequiredIdProperty().getPropertyName(); + IdDescription idDescription = targetNodeDescription.getIdDescription(); + boolean assignedId = idDescription != null + && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); + binderFunction = binderFunction.andThen(tree -> { + @SuppressWarnings("unchecked") + Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); - if (properties != null && !includeProperty.isNotFiltering()) { - properties.entrySet().removeIf(e -> { - // we cannot skip the id property if it is an assigned id - boolean isIdProperty = e.getKey().equals(idPropertyName); - return !(assignedId && isIdProperty) && !includeProperty.contains(currentPropertyPath.append(e.getKey())); - }); - } - return tree; + if (properties != null && !includeProperty.isNotFiltering()) { + properties.entrySet().removeIf(e -> { + // we cannot skip the id property if it is an assigned id + boolean isIdProperty = e.getKey().equals(idPropertyName); + return !(assignedId && isIdProperty) + && !includeProperty.contains(currentPropertyPath.append(e.getKey())); }); - return neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) - .bind(entity).with(binderFunction) - .fetchAs(Entity.class) - .one(); - }).switchIfEmpty(Mono.defer(() -> { - if (targetNodeDescription.hasVersionProperty()) { - return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); - } - return Mono.empty(); - })); + } + return tree; + }); + return this.neo4jClient + .query(() -> this.renderer.render(this.cypherGenerator.prepareSaveOf(targetNodeDescription, + dynamicLabels, TemplateSupport.rendererRendersElementId(this.renderer)))) + .bind(entity) + .with(binderFunction) + .fetchAs(Entity.class) + .one(); + }).switchIfEmpty(Mono.defer(() -> { + if (targetNodeDescription.hasVersionProperty()) { + return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)); + } + return Mono.empty(); + })); } @Override @@ -1224,37 +1363,44 @@ public final class ReactiveNeo4jTemplate implements Map finalParameters = queryFragmentsAndParameters.getParameters(); QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments(); - Neo4jPersistentEntity entityMetaData = (Neo4jPersistentEntity) queryFragmentsAndParameters.getNodeDescription(); + Neo4jPersistentEntity entityMetaData = (Neo4jPersistentEntity) queryFragmentsAndParameters + .getNodeDescription(); - boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField); + boolean containsPossibleCircles = entityMetaData != null + && entityMetaData.containsPossibleCircles(queryFragments::includeField); if (cypherQuery == null || containsPossibleCircles) { if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) { - return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters) - .map(nodesAndRelationshipsById -> { - var statement = nodesAndRelationshipsById.toStatement(entityMetaData); - ReactiveNeo4jClient.MappingSpec mappingSpec = this.neo4jClient - .query(renderer.render(statement)) - .bindAll(statement.getCatalog().getParameters()) - .fetchAs(resultType); + return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, + finalParameters) + .map(nodesAndRelationshipsById -> { + var statement = nodesAndRelationshipsById.toStatement(entityMetaData); + ReactiveNeo4jClient.MappingSpec mappingSpec = this.neo4jClient + .query(this.renderer.render(statement)) + .bindAll(statement.getCatalog().getParameters()) + .fetchAs(resultType); - ReactiveNeo4jClient.RecordFetchSpec fetchSpec = preparedQuery.getOptionalMappingFunction() - .map(mappingSpec::mappedBy).orElse(mappingSpec); + ReactiveNeo4jClient.RecordFetchSpec fetchSpec = preparedQuery + .getOptionalMappingFunction() + .map(mappingSpec::mappedBy) + .orElse(mappingSpec); - return new DefaultReactiveExecutableQuery<>(preparedQuery, fetchSpec); - }); + return new DefaultReactiveExecutableQuery<>(preparedQuery, fetchSpec); + }); } Statement statement = queryFragments.toStatement(); - cypherQuery = renderer.render(statement); + cypherQuery = this.renderer.render(statement); finalParameters = TemplateSupport.mergeParameters(statement, finalParameters); } ReactiveNeo4jClient.MappingSpec mappingSpec = this.neo4jClient.query(cypherQuery) - .bindAll(finalParameters).fetchAs(resultType); + .bindAll(finalParameters) + .fetchAs(resultType); ReactiveNeo4jClient.RecordFetchSpec fetchSpec = preparedQuery.getOptionalMappingFunction() - .map(mappingSpec::mappedBy).orElse(mappingSpec); + .map(mappingSpec::mappedBy) + .orElse(mappingSpec); return Mono.just(new DefaultReactiveExecutableQuery<>(preparedQuery, fetchSpec)); }); @@ -1263,19 +1409,19 @@ public final class ReactiveNeo4jTemplate implements @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.eventSupport = ReactiveEventSupport.discoverCallbacks(neo4jMappingContext, beanFactory); + this.eventSupport = ReactiveEventSupport.discoverCallbacks(this.neo4jMappingContext, beanFactory); SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader)); spelAwareProxyProjectionFactory.setBeanFactory(beanFactory); this.projectionFactory = spelAwareProxyProjectionFactory; - Configuration cypherDslConfiguration = beanFactory - .getBeanProvider(Configuration.class) - .getIfAvailable(Configuration::defaultConfig); + Configuration cypherDslConfiguration = beanFactory.getBeanProvider(Configuration.class) + .getIfAvailable(Configuration::defaultConfig); this.renderer = Renderer.getRenderer(cypherDslConfiguration); - this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect()); - this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction); + this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction + .apply(cypherDslConfiguration.getDialect()); + this.cypherGenerator.setElementIdOrIdFunction(this.elementIdOrIdFunction); if (this.transactionalOperator != null && this.transactionalOperatorReadOnly != null) { return; @@ -1287,8 +1433,9 @@ public final class ReactiveNeo4jTemplate implements ReactiveTransactionManager transactionManagerCandidate = iter.next(); if (transactionManagerCandidate instanceof ReactiveNeo4jTransactionManager reactiveNeo4jTransactionManager) { if (reactiveTransactionManager != null) { - throw new IllegalStateException("Multiple ReactiveNeo4jTransactionManagers are defined in this context. " + - "If this in intended, please pass the transaction manager to use with this ReactiveNeo4jTemplate in the constructor"); + throw new IllegalStateException( + "Multiple ReactiveNeo4jTransactionManagers are defined in this context. " + + "If this in intended, please pass the transaction manager to use with this ReactiveNeo4jTemplate in the constructor"); } reactiveTransactionManager = reactiveNeo4jTransactionManager; } @@ -1301,12 +1448,14 @@ public final class ReactiveNeo4jTemplate implements return; } this.transactionalOperator = TransactionalOperator.create(reactiveTransactionManager); - this.transactionalOperatorReadOnly = TransactionalOperator.create(reactiveTransactionManager, readOnlyTransactionDefinition); + this.transactionalOperatorReadOnly = TransactionalOperator.create(reactiveTransactionManager, + readOnlyTransactionDefinition); } @Override public void setBeanClassLoader(ClassLoader classLoader) { - this.beanClassLoader = beanClassLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : beanClassLoader; + this.beanClassLoader = (this.beanClassLoader != null) ? this.beanClassLoader + : org.springframework.util.ClassUtils.getDefaultClassLoader(); } @Override @@ -1321,40 +1470,41 @@ public final class ReactiveNeo4jTemplate implements final class DefaultReactiveExecutableQuery implements ExecutableQuery { private final PreparedQuery preparedQuery; + private final ReactiveNeo4jClient.RecordFetchSpec fetchSpec; - DefaultReactiveExecutableQuery(PreparedQuery preparedQuery, ReactiveNeo4jClient.RecordFetchSpec fetchSpec) { + DefaultReactiveExecutableQuery(PreparedQuery preparedQuery, + ReactiveNeo4jClient.RecordFetchSpec fetchSpec) { this.preparedQuery = preparedQuery; this.fetchSpec = fetchSpec; } - /** - * @return All results returned by this query. - */ + @Override @SuppressWarnings("unchecked") public Flux getResults() { - return execute(fetchSpec.all().switchOnFirst((signal, f) -> { - if (signal.hasValue() && preparedQuery.resultsHaveBeenAggregated()) { + return execute(this.fetchSpec.all().switchOnFirst((signal, f) -> { + if (signal.hasValue() && this.preparedQuery.resultsHaveBeenAggregated()) { return f.concatMap(nested -> Flux.fromIterable((Collection) nested).distinct()).distinct(); } return f; })); } - /** - * @return A single result - * @throws IncorrectResultSizeDataAccessException if there is no or more than one result - */ + @Override public Mono getSingleResult() { - return execute(fetchSpec.one().map(t -> { + return execute(this.fetchSpec.one().map(t -> { if (t instanceof LinkedHashSet) { @SuppressWarnings("unchecked") T firstItem = (T) ((LinkedHashSet) t).iterator().next(); return firstItem; } return t; - }).onErrorMap(IndexOutOfBoundsException.class, e -> new IncorrectResultSizeDataAccessException(Objects.requireNonNull(e.getMessage()), 1))); + }) + .onErrorMap(IndexOutOfBoundsException.class, + e -> new IncorrectResultSizeDataAccessException(Objects.requireNonNull(e.getMessage()), 1))); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveUserSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveUserSelectionProvider.java index 34bb6bbff..e89b0312c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveUserSelectionProvider.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveUserSelectionProvider.java @@ -15,36 +15,27 @@ */ package org.springframework.data.neo4j.core; +import org.apiguardian.api.API; import reactor.core.publisher.Mono; -import org.apiguardian.api.API; - /** + * Functional interface for dynamic provision of usernames to the system. + * * @author Michael J. Simons - * @soundtrack Tori Amos - Strange Little Girls * @since 6.2 */ @API(status = API.Status.STABLE, since = "6.2") public interface ReactiveUserSelectionProvider { - Mono getUserSelection(); - /** * A user selection provider always selecting the connected user. - * - * @return A provider for using the connected user. + * @return a provider for using the connected user. */ static ReactiveUserSelectionProvider getDefaultSelectionProvider() { return DefaultReactiveUserSelectionProvider.INSTANCE; } -} -enum DefaultReactiveUserSelectionProvider implements ReactiveUserSelectionProvider { - INSTANCE; + Mono getUserSelection(); - @Override - public Mono getUserSelection() { - return Mono.just(UserSelection.connectedUser()); - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java b/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java index ec770e41e..26851a3f4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java +++ b/src/main/java/org/springframework/data/neo4j/core/RelationshipHandler.java @@ -15,20 +15,21 @@ */ package org.springframework.data.neo4j.core; -import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; -import org.springframework.core.CollectionFactory; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; - import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Optional; +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.CollectionFactory; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; + /** - * Internal helper class that takes care of tracking whether a related object or a collection of related objects was recreated - * due to changing immutable properties + * Internal helper class that takes care of tracking whether a related object or a + * collection of related objects was recreated due to changing immutable properties. * * @author Michael J. Simons */ @@ -37,12 +38,27 @@ final class RelationshipHandler { private static final int DEFAULT_SIZE = 32; - enum Cardinality { + private final Neo4jPersistentProperty property; - ONE_TO_ONE, - ONE_TO_MANY, - DYNAMIC_ONE_TO_ONE, - DYNAMIC_ONE_TO_MANY + /** + * The raw value as passed to the template. + */ + @Nullable + private final Object rawValue; + + private final Cardinality cardinality; + + private final Map newRelatedObjectsByType; + + private Collection newRelatedObjects; + + RelationshipHandler(Neo4jPersistentProperty property, @Nullable Object rawValue, Cardinality cardinality, + Collection newRelatedObjects, Map newRelatedObjectsByType) { + this.property = property; + this.rawValue = rawValue; + this.cardinality = cardinality; + this.newRelatedObjects = newRelatedObjects; + this.newRelatedObjectsByType = newRelatedObjectsByType; } static RelationshipHandler forProperty(Neo4jPersistentProperty property, @Nullable Object rawValue) { @@ -51,68 +67,55 @@ final class RelationshipHandler { Collection newRelationshipObjectCollection = Collections.emptyList(); Map newRelationshipObjectCollectionMap = Collections.emptyMap(); - // Order is important here, all map based associations are dynamic, but not all dynamic associations are one to many + // Order is important here, all map based associations are dynamic, but not all + // dynamic associations are one to many if (property.isCollectionLike()) { cardinality = Cardinality.ONE_TO_MANY; - var size = rawValue == null ? DEFAULT_SIZE : ((Collection) rawValue).size(); + var size = (rawValue != null) ? ((Collection) rawValue).size() : DEFAULT_SIZE; newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), size); - } else if (property.isDynamicOneToManyAssociation()) { + } + else if (property.isDynamicOneToManyAssociation()) { cardinality = Cardinality.DYNAMIC_ONE_TO_MANY; - var size = rawValue == null ? DEFAULT_SIZE : ((Map) rawValue).size(); + var size = (rawValue != null) ? ((Map) rawValue).size() : DEFAULT_SIZE; newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size); - } else if (property.isDynamicAssociation()) { + } + else if (property.isDynamicAssociation()) { cardinality = Cardinality.DYNAMIC_ONE_TO_ONE; - var size = rawValue == null ? DEFAULT_SIZE : ((Map) rawValue).size(); + var size = (rawValue != null) ? ((Map) rawValue).size() : DEFAULT_SIZE; newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size); - } else { + } + else { cardinality = Cardinality.ONE_TO_ONE; } - return new RelationshipHandler(property, rawValue, cardinality, newRelationshipObjectCollection, newRelationshipObjectCollectionMap); - } - - private final Neo4jPersistentProperty property; - /** - * The raw value as passed to the template. - */ - @Nullable - private final Object rawValue; - private final Cardinality cardinality; - - private Collection newRelatedObjects; - private final Map newRelatedObjectsByType; - - RelationshipHandler(Neo4jPersistentProperty property, - @Nullable Object rawValue, Cardinality cardinality, - Collection newRelatedObjects, - Map newRelatedObjectsByType) { - this.property = property; - this.rawValue = rawValue; - this.cardinality = cardinality; - this.newRelatedObjects = newRelatedObjects; - this.newRelatedObjectsByType = newRelatedObjectsByType; + return new RelationshipHandler(property, rawValue, cardinality, newRelationshipObjectCollection, + newRelationshipObjectCollectionMap); } void handle(Object relatedValueToStore, Object newRelatedObject, Object potentiallyRecreatedRelatedObject) { if (potentiallyRecreatedRelatedObject != newRelatedObject) { - if (cardinality == Cardinality.ONE_TO_ONE) { + if (this.cardinality == Cardinality.ONE_TO_ONE) { this.newRelatedObjects = Collections.singletonList(potentiallyRecreatedRelatedObject); - } else if (cardinality == Cardinality.ONE_TO_MANY) { - newRelatedObjects.add(potentiallyRecreatedRelatedObject); - } else { + } + else if (this.cardinality == Cardinality.ONE_TO_MANY) { + this.newRelatedObjects.add(potentiallyRecreatedRelatedObject); + } + else { Object key = ((Map.Entry) relatedValueToStore).getKey(); - if (cardinality == Cardinality.DYNAMIC_ONE_TO_ONE) { - newRelatedObjectsByType.put(key, potentiallyRecreatedRelatedObject); - } else { + if (this.cardinality == Cardinality.DYNAMIC_ONE_TO_ONE) { + this.newRelatedObjectsByType.put(key, potentiallyRecreatedRelatedObject); + } + else { @SuppressWarnings("unchecked") - Collection newCollection = (Collection) newRelatedObjectsByType - .computeIfAbsent(key, k -> { - Collection objects = rawValue == null ? null : (Collection) ((Map) rawValue).get(key); - return CollectionFactory.createCollection( - property.getTypeInformation().getRequiredActualType().getType(), - objects != null ? objects.size() : DEFAULT_SIZE); - }); + Collection newCollection = (Collection) this.newRelatedObjectsByType + .computeIfAbsent(key, k -> { + Collection objects = (this.rawValue != null) + ? (Collection) ((Map) this.rawValue).get(key) : null; + return CollectionFactory.createCollection( + this.property.getTypeInformation().getRequiredActualType().getType(), + (objects != null) ? objects.size() : DEFAULT_SIZE); + }); newCollection.add(potentiallyRecreatedRelatedObject); } } @@ -122,25 +125,34 @@ final class RelationshipHandler { void applyFinalResultToOwner(PersistentPropertyAccessor parentPropertyAccessor) { Object finalRelation = null; - switch (cardinality) { + switch (this.cardinality) { case ONE_TO_ONE: - finalRelation = Optional.ofNullable(newRelatedObjects).flatMap(v -> v.stream().findFirst()).orElse(null); + finalRelation = Optional.ofNullable(this.newRelatedObjects) + .flatMap(v -> v.stream().findFirst()) + .orElse(null); break; case ONE_TO_MANY: - if (!newRelatedObjects.isEmpty()) { - finalRelation = newRelatedObjects; + if (!this.newRelatedObjects.isEmpty()) { + finalRelation = this.newRelatedObjects; } break; case DYNAMIC_ONE_TO_ONE: case DYNAMIC_ONE_TO_MANY: - if (!newRelatedObjectsByType.isEmpty()) { - finalRelation = newRelatedObjectsByType; + if (!this.newRelatedObjectsByType.isEmpty()) { + finalRelation = this.newRelatedObjectsByType; } break; } if (finalRelation != null) { - parentPropertyAccessor.setProperty(property, finalRelation); + parentPropertyAccessor.setProperty(this.property, finalRelation); } } + + enum Cardinality { + + ONE_TO_ONE, ONE_TO_MANY, DYNAMIC_ONE_TO_ONE, DYNAMIC_ONE_TO_MANY + + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java b/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java index e69e7cef3..8cd1e33c2 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java +++ b/src/main/java/org/springframework/data/neo4j/core/ResultSummaries.java @@ -29,35 +29,54 @@ import org.neo4j.driver.summary.InputPosition; import org.neo4j.driver.summary.Notification; import org.neo4j.driver.summary.Plan; import org.neo4j.driver.summary.ResultSummary; + import org.springframework.core.log.LogAccessor; /** * Utility class for dealing with result summaries. * * @author Michael J. Simons - * @soundtrack Fatoni & Dexter - Yo, Picasso * @since 6.0 */ final class ResultSummaries { private static final String LINE_SEPARATOR = System.lineSeparator(); - private static final LogAccessor cypherPerformanceNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.performance")); - private static final LogAccessor cypherHintNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.hint")); - private static final LogAccessor cypherUnrecognizedNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.unrecognized")); - private static final LogAccessor cypherUnsupportedNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.unsupported")); - private static final LogAccessor cypherDeprecationNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.deprecation")); - private static final LogAccessor cypherGenericNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.generic")); - private static final LogAccessor cypherSecurityNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.security")); - private static final LogAccessor cypherTopologyNotificationLog = new LogAccessor(LogFactory.getLog("org.springframework.data.neo4j.cypher.topology")); - private static final Pattern DEPRECATED_ID_PATTERN = Pattern.compile("(?im)The query used a deprecated function[\\.:] \\(?[`']id.+"); + private static final LogAccessor cypherPerformanceNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.performance")); + + private static final LogAccessor cypherHintNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.hint")); + + private static final LogAccessor cypherUnrecognizedNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.unrecognized")); + + private static final LogAccessor cypherUnsupportedNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.unsupported")); + + private static final LogAccessor cypherDeprecationNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.deprecation")); + + private static final LogAccessor cypherGenericNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.generic")); + + private static final LogAccessor cypherSecurityNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.security")); + + private static final LogAccessor cypherTopologyNotificationLog = new LogAccessor( + LogFactory.getLog("org.springframework.data.neo4j.cypher.topology")); + + private static final Pattern DEPRECATED_ID_PATTERN = Pattern + .compile("(?im)The query used a deprecated function[.:] \\(?[`']id.+"); + + private ResultSummaries() { + } /** - * Does some post-processing on the giving result summary, especially logging all notifications - * and potentially query plans. - * - * @param resultSummary The result summary to process - * @return The same, unmodified result summary. + * Does some post-processing on the giving result summary, especially logging all + * notifications and potentially query plans. + * @param resultSummary the result summary to process + * @return the same, unmodified result summary. */ static ResultSummary process(ResultSummary resultSummary) { logNotifications(resultSummary); @@ -75,34 +94,39 @@ final class ResultSummaries { Predicate isDeprecationWarningForId; try { isDeprecationWarningForId = notification -> supressIdDeprecations - && notification.classification().orElse(NotificationClassification.UNRECOGNIZED) - == NotificationClassification.DEPRECATION && DEPRECATED_ID_PATTERN.matcher(notification.description()) - .matches(); - } finally { + && notification.classification() + .orElse(NotificationClassification.UNRECOGNIZED) == NotificationClassification.DEPRECATION + && DEPRECATED_ID_PATTERN.matcher(notification.description()).matches(); + } + finally { Neo4jClient.SUPPRESS_ID_DEPRECATIONS.setRelease(supressIdDeprecations); } String query = resultSummary.query().text(); resultSummary.notifications() - .stream().filter(Predicate.not(isDeprecationWarningForId)) - .forEach(notification -> notification.severityLevel().ifPresent(severityLevel -> { - var category = notification.classification().orElse(null); + .stream() + .filter(Predicate.not(isDeprecationWarningForId)) + .forEach(notification -> notification.severityLevel().ifPresent(severityLevel -> { + var category = notification.classification().orElse(null); - var logger = getLogAccessor(category); - Consumer logFunction; - if (severityLevel == NotificationSeverity.WARNING) { - logFunction = logger::warn; - } else if (severityLevel == NotificationSeverity.INFORMATION) { - logFunction = logger::info; - } else if (severityLevel == NotificationSeverity.OFF) { - logFunction = (String message) -> { - }; - } else { - logFunction = logger::debug; - } + var logger = getLogAccessor(category); + Consumer logFunction; + if (severityLevel == NotificationSeverity.WARNING) { + logFunction = logger::warn; + } + else if (severityLevel == NotificationSeverity.INFORMATION) { + logFunction = logger::info; + } + else if (severityLevel == NotificationSeverity.OFF) { + logFunction = (String message) -> { + }; + } + else { + logFunction = logger::debug; + } - logFunction.accept(ResultSummaries.format(notification, query)); - })); + logFunction.accept(ResultSummaries.format(notification, query)); + })); } private static LogAccessor getLogAccessor(@Nullable NotificationClassification category) { @@ -124,10 +148,9 @@ final class ResultSummaries { /** * Creates a formatted string for a notification issued for a given query. - * - * @param notification The notification to format - * @param forQuery The query that caused the notification - * @return A formatted string + * @param notification the notification to format + * @param forQuery the query that caused the notification + * @return a formatted string */ static String format(Notification notification, String forQuery) { @@ -140,8 +163,10 @@ final class ResultSummaries { String line = lines[i]; queryHint.append("\t").append(line).append(LINE_SEPARATOR); if (hasPosition && i + 1 == position.line()) { - queryHint.append("\t").append(Stream.generate(() -> " ").limit(position.column() - 1) - .collect(Collectors.joining())).append("^").append(System.lineSeparator()); + queryHint.append("\t") + .append(Stream.generate(() -> " ").limit(position.column() - 1).collect(Collectors.joining())) + .append("^") + .append(System.lineSeparator()); } } return String.format("%s: %s%n%s%s", notification.code(), notification.title(), queryHint, @@ -150,8 +175,7 @@ final class ResultSummaries { /** * Logs the plan of the result summary if available and log level is at least debug. - * - * @param resultSummary The result summary that might contain a plan + * @param resultSummary the result summary that might contain a plan */ private static void logPlan(ResultSummary resultSummary) { @@ -180,6 +204,4 @@ final class ResultSummaries { } } - private ResultSummaries() { - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java index bf8fc68f4..08faeb2ef 100644 --- a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java +++ b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java @@ -22,13 +22,15 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.convert.ConversionService; /** - * Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}. + * Used to automatically map single valued records to a sensible Java type based on + * {@link Value#asObject()}. * - * @author Michael J. Simons * @param type of the domain class to map + * @author Michael J. Simons * @since 6.0 */ final class SingleValueMappingFunction implements BiFunction { @@ -43,8 +45,7 @@ final class SingleValueMappingFunction implements BiFunction implements BiFunction findCommonElementType(Iterable collection) { + @Nullable public static Class findCommonElementType(Iterable collection) { if (collection == null) { return null; } Collection> allClasses = StreamSupport.stream(collection.spliterator(), true) - .filter(Objects::nonNull) - .map(Object::getClass).collect(Collectors.toSet()); + .filter(Objects::nonNull) + .map(Object::getClass) + .collect(Collectors.toSet()); if (allClasses.isEmpty()) { return EmptyIterable.class; @@ -110,7 +99,8 @@ public final class TemplateSupport { for (Class type : allClasses) { if (candidate == null) { candidate = type; - } else if (candidate != type) { + } + else if (candidate != type) { candidate = null; break; } @@ -118,7 +108,8 @@ public final class TemplateSupport { if (candidate != null) { return candidate; - } else { + } + else { Predicate> moveUp = c -> c != null && c != Object.class; Set> mostAbstractClasses = new HashSet<>(); for (Class type : allClasses) { @@ -127,48 +118,46 @@ public final class TemplateSupport { } mostAbstractClasses.add(type); } - candidate = mostAbstractClasses.size() == 1 ? mostAbstractClasses.iterator().next() : null; + candidate = (mostAbstractClasses.size() != 1) ? null : mostAbstractClasses.iterator().next(); } if (candidate != null) { return candidate; - } else { + } + else { List>> interfacesPerClass = allClasses.stream() - .map(c -> Arrays.stream(c.getInterfaces()).collect(Collectors.toSet())) - .collect(Collectors.toList()); + .map(c -> Arrays.stream(c.getInterfaces()).collect(Collectors.toSet())) + .collect(Collectors.toList()); Set> allInterfaces = interfacesPerClass.stream().flatMap(Set::stream).collect(Collectors.toSet()); interfacesPerClass - .forEach(setOfInterfaces -> allInterfaces.removeIf(iface -> !setOfInterfaces.contains(iface))); - candidate = allInterfaces.size() == 1 ? allInterfaces.iterator().next() : null; + .forEach(setOfInterfaces -> allInterfaces.removeIf(iface -> !setOfInterfaces.contains(iface))); + candidate = (allInterfaces.size() != 1) ? null : allInterfaces.iterator().next(); } return candidate; } static PropertyFilter computeIncludePropertyPredicate(Collection includedProperties, - NodeDescription nodeDescription) { + NodeDescription nodeDescription) { return PropertyFilter.from(includedProperties, nodeDescription); } - static void updateVersionPropertyIfPossible( - Neo4jPersistentEntity entityMetaData, - PersistentPropertyAccessor propertyAccessor, - Entity newOrUpdatedNode - ) { + static void updateVersionPropertyIfPossible(Neo4jPersistentEntity entityMetaData, + PersistentPropertyAccessor propertyAccessor, Entity newOrUpdatedNode) { if (entityMetaData.hasVersionProperty()) { var versionProperty = entityMetaData.getRequiredVersionProperty(); - propertyAccessor.setProperty( - versionProperty, newOrUpdatedNode.get(versionProperty.getPropertyName()).asLong()); + propertyAccessor.setProperty(versionProperty, + newOrUpdatedNode.get(versionProperty.getPropertyName()).asLong()); } } /** - * Merges statement and explicit parameters. Statement parameters have a higher precedence - * - * @param statement A statement that maybe has some stored parameters - * @param parameters The original parameters - * @return Merged parameters + * Merges statement and explicit parameters. Statement parameters have a higher + * precedence + * @param statement a statement that maybe has some stored parameters + * @param parameters the original parameters + * @return the merged parameters */ static Map mergeParameters(Statement statement, Map parameters) { @@ -180,94 +169,27 @@ public final class TemplateSupport { } /** - * Parameter holder class for a query with the return pattern of `rootNodes, relationships, relatedNodes`. - * The parameter values must be internal node or relationship ids. - */ - static final class NodesAndRelationshipsByIdStatementProvider { - - private final static String ROOT_NODE_IDS = "rootNodeIds"; - private final static String RELATIONSHIP_IDS = "relationshipIds"; - private final static String RELATED_NODE_IDS = "relatedNodeIds"; - - final static NodesAndRelationshipsByIdStatementProvider EMPTY = - new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments(), SpringDataCypherDsl.elementIdOrIdFunction.apply(Dialect.NEO4J_4)); - - private final Map> parameters = new HashMap<>(3); - private final QueryFragments queryFragments; - private final Function elementIdFunction; - - NodesAndRelationshipsByIdStatementProvider(Collection rootNodeIds, Collection relationshipsIds, Collection relatedNodeIds, QueryFragments queryFragments, Function elementIdFunction) { - - this.elementIdFunction = elementIdFunction; - this.parameters.put(ROOT_NODE_IDS, rootNodeIds); - this.parameters.put(RELATIONSHIP_IDS, relationshipsIds); - this.parameters.put(RELATED_NODE_IDS, relatedNodeIds); - this.queryFragments = queryFragments; - - } - - boolean hasRootNodeIds() { - var ids = parameters.get(ROOT_NODE_IDS); - return ids != null && !ids.isEmpty(); - } - - Statement toStatement(NodeDescription nodeDescription) { - - String primaryLabel = nodeDescription.getPrimaryLabel(); - Node rootNodes = Cypher.node(primaryLabel).named(ROOT_NODE_IDS); - Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS); - - List projection = new ArrayList<>(); - projection.add(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); - projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS)); - projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)); - projection.addAll(queryFragments.getAdditionalReturnExpressions()); - - Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS); - return Cypher.match(rootNodes) - .where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS))))) - .with(Cypher.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE)) - .optionalMatch(relationships) - .where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS))))) - .with(Constants.NAME_OF_ROOT_NODE, Cypher.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS)) - .optionalMatch(relatedNodes) - .where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS))))) - .with( - Constants.NAME_OF_ROOT_NODE, - Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS), - Cypher.collectDistinct(relatedNodes).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES) - ) - .unwind(Constants.NAME_OF_ROOT_NODE).as(ROOT_NODE_IDS) - .with( - Cypher.name(ROOT_NODE_IDS).as(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).getValue()), - Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS), - Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)) - .orderBy(queryFragments.getOrderBy()) - .returning(projection) - .skip(queryFragments.getSkip()) - .limit(queryFragments.getLimit()).build(); - } - } - - /** - * Checks if the {@code domainType} is a known entity in the {@code mappingContext} and retrieves the mapping function - * for it. If the {@code resultType} is not an interface, a DTO based projection further down the chain is assumed - * and therefore a call to {@link EntityInstanceWithSource#decorateMappingFunction(BiFunction)} is made, so that - * a {@link org.springframework.data.neo4j.core.mapping.DtoInstantiatingConverter} can be used with the query result. - * - * @param mappingContext Needed for retrieving the original mapping function - * @param domainType The actual domain type (a {@link org.springframework.data.neo4j.core.schema.Node}). - * @param resultType An optional different result type - * @param The domain type - * @return A mapping function + * Checks if the {@code domainType} is a known entity in the {@code mappingContext} + * and retrieves the mapping function for it. If the {@code resultType} is not an + * interface, a DTO based projection further down the chain is assumed and therefore a + * call to {@link EntityInstanceWithSource#decorateMappingFunction(BiFunction)} is + * made, so that a + * {@link org.springframework.data.neo4j.core.mapping.DtoInstantiatingConverter} can + * be used with the query result. + * @param mappingContext needed for retrieving the original mapping function + * @param domainType the actual domain type (a + * {@link org.springframework.data.neo4j.core.schema.Node}). + * @param resultType an optional different result type + * @param the domain type + * @return a mapping function */ static Supplier> getAndDecorateMappingFunction( Neo4jMappingContext mappingContext, Class domainType, @Nullable Class resultType) { Assert.notNull(mappingContext.getPersistentEntity(domainType), "Cannot get or create persistent entity"); return () -> { - BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor( - domainType); + BiFunction mappingFunction = mappingContext + .getRequiredMappingFunctionFor(domainType); if (resultType != null && domainType != resultType && !resultType.isInterface()) { mappingFunction = EntityInstanceWithSource.decorateMappingFunction(mappingFunction); } @@ -276,129 +198,109 @@ public final class TemplateSupport { } /** - * Computes a {@link PropertyFilter} from a set of included properties based on an entities meta data and applies it - * to a given binder function. - * - * @param includedProperties The set of included properties - * @param entityMetaData The metadata of the entity in question - * @param binderFunction The original binder function for persisting the entity. - * @param The type of the entity - * @return A new binder function that only works on the included properties. + * Computes a {@link PropertyFilter} from a set of included properties based on an + * entities meta data and applies it to a given binder function. + * @param includedProperties the set of included properties + * @param entityMetaData the metadata of the entity in question + * @param binderFunction the original binder function for persisting the entity. + * @param the type of the entity + * @return a new binder function that only works on the included properties. */ static FilteredBinderFunction createAndApplyPropertyFilter( Collection includedProperties, Neo4jPersistentEntity entityMetaData, Function> binderFunction) { - PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData); + PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, + entityMetaData); return new FilteredBinderFunction<>(includeProperty, binderFunction.andThen(tree -> { @SuppressWarnings("unchecked") Map properties = (Map) tree.get(Constants.NAME_OF_PROPERTIES_PARAM); String idPropertyName = entityMetaData.getRequiredIdProperty().getPropertyName(); IdDescription idDescription = entityMetaData.getIdDescription(); - boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); + boolean assignedId = idDescription != null + && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId()); if (!(includeProperty.isNotFiltering() || properties == null)) { - properties.entrySet() - .removeIf(e -> { - // we cannot skip the id property if it is an assigned id - boolean isIdProperty = e.getKey().equals(idPropertyName); - return !(assignedId && isIdProperty) && !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()); - }); + properties.entrySet().removeIf(e -> { + // we cannot skip the id property if it is an assigned id + boolean isIdProperty = e.getKey().equals(idPropertyName); + return !(assignedId && isIdProperty) + && !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()); + }); } return tree; })); } /** - * Helper function that computes the map of included properties for a dynamic projection as expected in 6.2, but - * for fully dynamic projection - * - * @param mappingContext The context to work on - * @param domainType The projected domain type - * @param predicate The predicate to compute the included columns - * @param Type of the domain type - * @return A map as expected by the property filter. + * Helper function that computes the map of included properties for a dynamic + * projection as expected in 6.2, but for fully dynamic projection. + * @param mappingContext the context to work on + * @param domainType the projected domain type + * @param predicate the predicate to compute the included columns + * @param the type of the domain type + * @return a map as expected by the property filter. */ - static Collection computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext, - Class domainType, BiPredicate predicate) { + static Collection computeIncludedPropertiesFromPredicate( + Neo4jMappingContext mappingContext, Class domainType, + BiPredicate predicate) { if (predicate == null) { return Collections.emptySet(); } Collection pps = new HashSet<>(); PropertyTraverser traverser = new PropertyTraverser(mappingContext); - traverser.traverse(domainType, predicate, (path, property) -> pps.add(new PropertyFilter.ProjectedPath(PropertyFilter.RelaxedPropertyPath.withRootType(domainType).append(path.toDotPath()), false))); + traverser + .traverse(domainType, predicate, + (path, property) -> pps.add(new PropertyFilter.ProjectedPath( + PropertyFilter.RelaxedPropertyPath.withRootType(domainType).append(path.toDotPath()), + false))); return pps; } /** - * A wrapper around a {@link Function} from entity to {@link Map} which is filtered the {@link PropertyFilter} included as well. - * - * @param Type of the entity - */ - static class FilteredBinderFunction implements Function> { - final PropertyFilter filter; - - final Function> binderFunction; - - FilteredBinderFunction(PropertyFilter filter, Function> binderFunction) { - this.filter = filter; - this.binderFunction = binderFunction; - } - - @Override - public Map apply(T t) { - return binderFunction.apply(t); - } - } - - /** - * Uses the given {@link PersistentPropertyAccessor propertyAccessor} to set the value of the generated id. - * - * @param entityMetaData The type information from SDN - * @param propertyAccessor An accessor tied to a concrete instance - * @param elementId The element id to store - * @param databaseEntity A fallback entity to retrieve the deprecated internal long id - * @param The type of the entity + * Uses the given {@link PersistentPropertyAccessor propertyAccessor} to set the value + * of the generated id. + * @param entityMetaData the type information from SDN + * @param propertyAccessor an accessor tied to a concrete instance + * @param elementId the element id to store + * @param databaseEntity a fallback entity to retrieve the deprecated internal long id + * @param the type of the entity */ @SuppressWarnings("deprecation") - static void setGeneratedIdIfNecessary( - Neo4jPersistentEntity entityMetaData, - PersistentPropertyAccessor propertyAccessor, - Object elementId, - Optional databaseEntity - ) { + static void setGeneratedIdIfNecessary(Neo4jPersistentEntity entityMetaData, + PersistentPropertyAccessor propertyAccessor, Object elementId, Optional databaseEntity) { if (!entityMetaData.isUsingInternalIds()) { return; } var requiredIdProperty = entityMetaData.getRequiredIdProperty(); var idPropertyType = requiredIdProperty.getType(); if (entityMetaData.isUsingDeprecatedInternalId()) { - propertyAccessor.setProperty(requiredIdProperty, databaseEntity.map(IdentitySupport::getInternalId).orElseThrow()); - } else if (idPropertyType.equals(String.class)) { + propertyAccessor.setProperty(requiredIdProperty, + databaseEntity.map(IdentitySupport::getInternalId).orElseThrow()); + } + else if (idPropertyType.equals(String.class)) { propertyAccessor.setProperty(requiredIdProperty, elementId); - } else { + } + else { throw new IllegalArgumentException("Unsupported generated id property " + idPropertyType); } } /** - * Retrieves the object id for a related object if no has been found so far or updates the object with the id of a previously - * processed object. - * - * @param entityMetadata Needed for determining the type of ids - * @param propertyAccessor Bound to the currently processed entity - * @param databaseEntity Source for the old neo4j internal id - * @param relatedInternalId The element id or the string version of the old id - * @param The type of the entity - * @return The actual related internal id being used. + * Retrieves the object id for a related object if no has been found so far or updates + * the object with the id of a previously processed object. + * @param entityMetadata needed for determining the type of ids + * @param propertyAccessor bound to the currently processed entity + * @param databaseEntity source for the old neo4j internal id + * @param relatedInternalId the element id or the string version of the old id + * @param the type of the entity + * @return the actual related internal id being used. */ @SuppressWarnings("deprecation") - static Object retrieveOrSetRelatedId( - Neo4jPersistentEntity entityMetadata, + static Object retrieveOrSetRelatedId(Neo4jPersistentEntity entityMetadata, PersistentPropertyAccessor propertyAccessor, @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional databaseEntity, - @Nullable Object relatedInternalId - ) { + @Nullable Object relatedInternalId) { if (!entityMetadata.isUsingInternalIds()) { return Objects.requireNonNull(relatedInternalId); } @@ -409,14 +311,17 @@ public final class TemplateSupport { if (entityMetadata.isUsingDeprecatedInternalId()) { if (relatedInternalId == null && current != null) { relatedInternalId = current.toString(); - } else if (current == null) { + } + else if (current == null) { long internalId = databaseEntity.map(Entity::id).orElseThrow(); propertyAccessor.setProperty(requiredIdProperty, internalId); } - } else { + } + else { if (relatedInternalId == null && current != null) { relatedInternalId = current; - } else if (current == null) { + } + else if (current == null) { propertyAccessor.setProperty(requiredIdProperty, relatedInternalId); } } @@ -424,7 +329,10 @@ public final class TemplateSupport { } /** - * Checks if the renderer is configured in such a way that it will use element id or apply toString(id(n)) workaround. + * Checks if the renderer is configured in such a way that it will use element id or + * apply toString(id(n)) workaround. + * @param renderer the rendered to check + * @param targetEntity the entity that might use internal ids * @return {@literal true} if renderer will use elementId */ static boolean rendererCanUseElementIdIfPresent(Renderer renderer, Neo4jPersistentEntity targetEntity) { @@ -433,7 +341,7 @@ public final class TemplateSupport { static boolean rendererRendersElementId(Renderer renderer) { return renderer.render(Cypher.returning(Cypher.elementId(Cypher.anyNode("n"))).build()) - .equals("RETURN elementId(n)"); + .equals("RETURN elementId(n)"); } public static String convertIdOrElementIdToString(Object value) { @@ -447,36 +355,167 @@ public final class TemplateSupport { return value.toString(); } - @Nullable - static Object convertToLongIdOrStringElementId(@Nullable Collection ids) { + @Nullable static Object convertToLongIdOrStringElementId(@Nullable Collection ids) { if (ids == null) { return null; } try { - return ids.stream() - .map(Long::valueOf).collect(Collectors.toSet()); + return ids.stream().map(Long::valueOf).collect(Collectors.toSet()); - } catch (Exception e) { + } + catch (Exception ex) { return ids; } } - static Object convertIdValues(Neo4jMappingContext ctx, @Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) { + static Object convertIdValues(Neo4jMappingContext ctx, @Nullable Neo4jPersistentProperty idProperty, + @Nullable Object idValues) { if (idProperty != null && ((Neo4jPersistentEntity) idProperty.getOwner()).isUsingInternalIds()) { return (idValues != null) ? idValues : Values.NULL; } if (idValues != null) { - return ctx.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter()); - } else if (idProperty != null) { - return ctx.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter()); - } else { + return ctx.getConversionService() + .writeValue(idValues, TypeInformation.of(idValues.getClass()), + (idProperty != null) ? idProperty.getOptionalConverter() : null); + } + else if (idProperty != null) { + return ctx.getConversionService() + .writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter()); + } + else { // Not much we can convert here return Values.NULL; } } - private TemplateSupport() { + enum FetchType { + + ONE, ALL + } + + /** + * Indicator for an empty collection. + */ + public static final class EmptyIterable { + + private EmptyIterable() { + } + + } + + /** + * Parameter holder class for a query with the return pattern of `rootNodes, + * relationships, relatedNodes`. The parameter values must be internal node or + * relationship ids. + */ + static final class NodesAndRelationshipsByIdStatementProvider { + + static final NodesAndRelationshipsByIdStatementProvider EMPTY = new NodesAndRelationshipsByIdStatementProvider( + Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments(), + SpringDataCypherDsl.elementIdOrIdFunction.apply(Dialect.NEO4J_4)); + + private static final String ROOT_NODE_IDS = "rootNodeIds"; + + private static final String RELATIONSHIP_IDS = "relationshipIds"; + + private static final String RELATED_NODE_IDS = "relatedNodeIds"; + + private final Map> parameters = new HashMap<>(3); + + private final QueryFragments queryFragments; + + private final Function elementIdFunction; + + NodesAndRelationshipsByIdStatementProvider(Collection rootNodeIds, Collection relationshipsIds, + Collection relatedNodeIds, QueryFragments queryFragments, + Function elementIdFunction) { + + this.elementIdFunction = elementIdFunction; + this.parameters.put(ROOT_NODE_IDS, rootNodeIds); + this.parameters.put(RELATIONSHIP_IDS, relationshipsIds); + this.parameters.put(RELATED_NODE_IDS, relatedNodeIds); + this.queryFragments = queryFragments; + + } + + boolean hasRootNodeIds() { + var ids = this.parameters.get(ROOT_NODE_IDS); + return ids != null && !ids.isEmpty(); + } + + Statement toStatement(NodeDescription nodeDescription) { + + String primaryLabel = nodeDescription.getPrimaryLabel(); + Node rootNodes = Cypher.node(primaryLabel).named(ROOT_NODE_IDS); + Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS); + + List projection = new ArrayList<>(); + projection.add(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription) + .as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); + projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS)); + projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)); + projection.addAll(this.queryFragments.getAdditionalReturnExpressions()); + + Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS); + return Cypher.match(rootNodes) + .where(this.elementIdFunction.apply(rootNodes) + .in(Cypher.parameter(ROOT_NODE_IDS, + convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS))))) + .with(Cypher.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE)) + .optionalMatch(relationships) + .where(this.elementIdFunction.apply(relationships) + .in(Cypher.parameter(RELATIONSHIP_IDS, + convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS))))) + .with(Constants.NAME_OF_ROOT_NODE, + Cypher.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS)) + .optionalMatch(relatedNodes) + .where(this.elementIdFunction.apply(relatedNodes) + .in(Cypher.parameter(RELATED_NODE_IDS, + convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS))))) + .with(Constants.NAME_OF_ROOT_NODE, + Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS) + .as(Constants.NAME_OF_SYNTHESIZED_RELATIONS), + Cypher.collectDistinct(relatedNodes).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)) + .unwind(Constants.NAME_OF_ROOT_NODE) + .as(ROOT_NODE_IDS) + .with(Cypher.name(ROOT_NODE_IDS) + .as(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).getValue()), + Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS), + Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)) + .orderBy(this.queryFragments.getOrderBy()) + .returning(projection) + .skip(this.queryFragments.getSkip()) + .limit(this.queryFragments.getLimit()) + .build(); + } + + } + + /** + * A wrapper around a {@link Function} from entity to {@link Map} which is filtered + * the {@link PropertyFilter} included as well. + * + * @param the type of the entity + */ + static class FilteredBinderFunction implements Function> { + + final PropertyFilter filter; + + final Function> binderFunction; + + FilteredBinderFunction(PropertyFilter filter, Function> binderFunction) { + this.filter = filter; + this.binderFunction = binderFunction; + } + + @Override + public Map apply(T t) { + return this.binderFunction.apply(t); + } + + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/UserSelection.java b/src/main/java/org/springframework/data/neo4j/core/UserSelection.java index 22c3591d4..32cd1fa9c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/UserSelection.java +++ b/src/main/java/org/springframework/data/neo4j/core/UserSelection.java @@ -19,19 +19,22 @@ import java.util.Objects; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** - * This is a value object for a Neo4j user, potentially different from the user owning the physical Neo4j connection. To make use of - * this a minimum version of Neo4j 4.4 and Neo4j-Java-Driver 4.4 is required, otherwise any usage of {@link UserSelection#impersonate(String)} - * together with either the {@link UserSelectionProvider} or the {@link ReactiveUserSelectionProvider} will lead to runtime - * errors. + * This is a value object for a Neo4j user, potentially different from the user owning the + * physical Neo4j connection. To make use of this a minimum version of Neo4j 4.4 and + * Neo4j-Java-Driver 4.4 is required, otherwise any usage of + * {@link UserSelection#impersonate(String)} together with either the + * {@link UserSelectionProvider} or the {@link ReactiveUserSelectionProvider} will lead to + * runtime errors. *

- * Similar usage pattern like with the dynamic database selection are possible, for example tying - * a {@link UserSelectionProvider} into Spring Security and use the current user as a user to impersonate. + * Similar usage pattern like with the dynamic database selection are possible, for + * example tying a {@link UserSelectionProvider} into Spring Security and use the current + * user as a user to impersonate. * * @author Michael J. Simons - * @soundtrack Tori Amos - Strange Little Girls * @since 6.2 */ @API(status = API.Status.STABLE, since = "6.2") @@ -39,24 +42,6 @@ public final class UserSelection { private static final UserSelection CONNECTED_USER = new UserSelection(null); - /** - * @return A user selection that will just use the user owning the physical connection. - */ - public static UserSelection connectedUser() { - - return CONNECTED_USER; - } - - /** - * @param value The name of the user to impersonate - * @return A user selection representing an impersonated user. - */ - public static UserSelection impersonate(String value) { - - Assert.hasText(value, "Cannot impersonate user without username"); - return new UserSelection(value); - } - @Nullable private final String value; @@ -64,9 +49,29 @@ public final class UserSelection { this.value = value; } - @Nullable - public String getValue() { - return value; + /** + * Just use the connected user. + * @return a user selection that will just use the user owning the physical + * connection. + */ + public static UserSelection connectedUser() { + + return CONNECTED_USER; + } + + /** + * Impersonate another user. + * @param value the name of the user to impersonate + * @return a user selection representing an impersonated user. + */ + public static UserSelection impersonate(String value) { + + Assert.hasText(value, "Cannot impersonate user without username"); + return new UserSelection(value); + } + + @Nullable public String getValue() { + return this.value; } @Override @@ -78,11 +83,12 @@ public final class UserSelection { return false; } UserSelection that = (UserSelection) o; - return Objects.equals(value, that.value); + return Objects.equals(this.value, that.value); } @Override public int hashCode() { - return Objects.hash(value); + return Objects.hash(this.value); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/UserSelectionProvider.java b/src/main/java/org/springframework/data/neo4j/core/UserSelectionProvider.java index f4f0f8050..88ab8cad5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/UserSelectionProvider.java +++ b/src/main/java/org/springframework/data/neo4j/core/UserSelectionProvider.java @@ -18,32 +18,24 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; /** + * Functional interface for dynamic provision of usernames to the system. + * * @author Michael J. Simons - * @soundtrack Tori Amos - Strange Little Girls * @since 6.2 */ @API(status = API.Status.STABLE, since = "6.2") @FunctionalInterface public interface UserSelectionProvider { - UserSelection getUserSelection(); - /** * A user selection provider always selecting the connected user. - * - * @return A provider for using the connected user. + * @return a provider for using the connected user. */ static UserSelectionProvider getDefaultSelectionProvider() { return DefaultUserSelectionProvider.INSTANCE; } -} -enum DefaultUserSelectionProvider implements UserSelectionProvider { - INSTANCE; + UserSelection getUserSelection(); - @Override - public UserSelection getUserSelection() { - return UserSelection.connectedUser(); - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java index 405b2d2ca..3bfa894bc 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java @@ -45,6 +45,7 @@ import org.neo4j.driver.exceptions.value.LossyCoercion; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.ConverterRegistry; @@ -59,7 +60,8 @@ import org.springframework.util.StringUtils; /** * Additional types that are supported out of the box. Mostly all of - * {@link org.springframework.data.mapping.model.SimpleTypeHolder SimpleTypeHolder's} defaults. + * {@link org.springframework.data.mapping.model.SimpleTypeHolder SimpleTypeHolder's} + * defaults. * * @author Michael J. Simons * @author Gerrit Meier @@ -70,47 +72,79 @@ final class AdditionalTypes { static final List CONVERTERS; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + static { List hlp = new ArrayList<>(); - hlp.add(ConverterBuilder.reading(Value.class, boolean[].class, AdditionalTypes::asBooleanArray).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, Byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, Character.class, AdditionalTypes::asCharacter).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, char.class, AdditionalTypes::asCharacter).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, char[].class, AdditionalTypes::asCharArray).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, Date.class, AdditionalTypes::asDate).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, double[].class, AdditionalTypes::asDoubleArray).andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, boolean[].class, AdditionalTypes::asBooleanArray) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, Byte.class, AdditionalTypes::asByte) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, byte.class, AdditionalTypes::asByte) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Character.class, AdditionalTypes::asCharacter) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, char.class, AdditionalTypes::asCharacter) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, char[].class, AdditionalTypes::asCharArray) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, Date.class, AdditionalTypes::asDate) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, double[].class, AdditionalTypes::asDoubleArray) + .andWriting(Values::value)); hlp.add(new EnumConverter()); - hlp.add(ConverterBuilder.reading(Value.class, Float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, float[].class, AdditionalTypes::asFloatArray).andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Float.class, AdditionalTypes::asFloat) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, float.class, AdditionalTypes::asFloat) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, float[].class, AdditionalTypes::asFloatArray) + .andWriting(AdditionalTypes::value)); hlp.add(ConverterBuilder.reading(Value.class, Integer.class, Value::asInt).andWriting(Values::value)); hlp.add(ConverterBuilder.reading(Value.class, int.class, Value::asInt).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, int[].class, AdditionalTypes::asIntArray).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, Locale.class, AdditionalTypes::asLocale).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, long[].class, AdditionalTypes::asLongArray).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, Short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, short[].class, AdditionalTypes::asShortArray).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, String[].class, AdditionalTypes::asStringArray).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger).andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, int[].class, AdditionalTypes::asIntArray) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, Locale.class, AdditionalTypes::asLocale) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, long[].class, AdditionalTypes::asLongArray) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, Short.class, AdditionalTypes::asShort) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, short.class, AdditionalTypes::asShort) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, short[].class, AdditionalTypes::asShortArray) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, String[].class, AdditionalTypes::asStringArray) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger) + .andWriting(AdditionalTypes::value)); hlp.add(new TemporalAmountConverter()); - hlp.add(ConverterBuilder.reading(Value.class, Instant.class, AdditionalTypes::asInstant).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, UUID.class, AdditionalTypes::asUUID).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, URL.class, AdditionalTypes::asURL).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, URI.class, AdditionalTypes::asURI).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, TimeZone.class, AdditionalTypes::asTimeZone).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, ZoneId.class, AdditionalTypes::asZoneId).andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Instant.class, AdditionalTypes::asInstant) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, UUID.class, AdditionalTypes::asUUID) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, URL.class, AdditionalTypes::asURL) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, URI.class, AdditionalTypes::asURI) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, TimeZone.class, AdditionalTypes::asTimeZone) + .andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, ZoneId.class, AdditionalTypes::asZoneId) + .andWriting(AdditionalTypes::value)); hlp.add(ConverterBuilder.reading(Value.class, Entity.class, Value::asEntity)); hlp.add(ConverterBuilder.reading(Value.class, Node.class, Value::asNode)); hlp.add(ConverterBuilder.reading(Value.class, Relationship.class, Value::asRelationship)); hlp.add(ConverterBuilder.reading(Value.class, Map.class, Value::asMap).andWriting(AdditionalTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, Vector.class, AdditionalTypes::asVector).andWriting(AdditionalTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Vector.class, AdditionalTypes::asVector) + .andWriting(AdditionalTypes::value)); CONVERTERS = Collections.unmodifiableList(hlp); } + private AdditionalTypes() { + } + static Value value(Map map) { return Values.value(map); } @@ -158,8 +192,9 @@ final class AdditionalTypes { static URL asURL(Value value) { try { return new URL(value.asString()); - } catch (MalformedURLException e) { - throw new MappingException("Could not create URL from value: " + value.asString(), e); + } + catch (MalformedURLException ex) { + throw new MappingException("Could not create URL from value: " + value.asString(), ex); } } @@ -234,8 +269,6 @@ final class AdditionalTypes { return chars[0]; } - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; - static Date asDate(Value value) { return Date.from(DATE_TIME_FORMATTER.parse(value.asString(), Instant::from)); @@ -249,108 +282,6 @@ final class AdditionalTypes { return Values.value(DATE_TIME_FORMATTER.format(date.toInstant().atZone(ZoneOffset.UTC.normalized()))); } - @ReadingConverter - @WritingConverter - static final class EnumConverter implements GenericConverter { - - private final Set convertibleTypes; - - EnumConverter() { - Set tmp = new HashSet<>(); - tmp.add(new ConvertiblePair(Value.class, Enum.class)); - tmp.add(new ConvertiblePair(Enum.class, Value.class)); - this.convertibleTypes = Collections.unmodifiableSet(tmp); - } - - @Override - public Set getConvertibleTypes() { - return convertibleTypes; - } - - @SuppressWarnings({"raw", "unchecked"}) // Due to dynamic enum retrieval - @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - - if (source == null) { - return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; - } - - if (Value.class.isAssignableFrom(sourceType.getType())) { - return Enum.valueOf((Class) targetType.getType(), ((Value) source).asString()); - } else { - return Values.value(((Enum) source).name()); - } - } - } - - /** - * This is a workaround for the fact that Spring Data Commons requires {@link GenericConverter generic converters} to - * have a non-null convertible pair since 2.3. Without it, they get filtered out and thus not registered in a - * conversion service. We do this as an afterthought in - * {@link Neo4jConversions#registerConvertersIn(ConverterRegistry)}. - *

- * This class uses is a {@link GenericConverter} without a concrete pair of convertible types. By making it implement - * {@link ConditionalConverter} it works with Springs conversion service out of the box. - */ - static final class EnumArrayConverter implements GenericConverter, ConditionalConverter { - - private final EnumConverter delegate; - - EnumArrayConverter() { - this.delegate = new EnumConverter(); - } - - @Override - @Nullable - public Set getConvertibleTypes() { - return null; - } - - @Override - public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - if (Value.class.isAssignableFrom(sourceType.getType())) { - return describesSupportedEnumVariant(targetType); - } else if (Value.class.isAssignableFrom(targetType.getType())) { - return describesSupportedEnumVariant(sourceType); - } else { - return false; - } - } - - private static boolean describesSupportedEnumVariant(TypeDescriptor typeDescriptor) { - var elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor(); - return typeDescriptor.isArray() - && elementTypeDescriptor != null && Enum.class.isAssignableFrom(elementTypeDescriptor.getType()); - } - - @Override - @Nullable - public Object convert(@Nullable Object object, TypeDescriptor sourceType, TypeDescriptor targetType) { - - if (object == null) { - return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; - } - - if (Value.class.isAssignableFrom(sourceType.getType())) { - Value source = (Value) object; - - TypeDescriptor elementTypeDescriptor = Objects.requireNonNull(targetType.getElementTypeDescriptor()); - Object[] targetArray = (Object[]) Array.newInstance(elementTypeDescriptor.getType(), source.size()); - - Arrays.setAll(targetArray, - i -> delegate.convert(source.get(i), TypeDescriptor.valueOf(Value.class), elementTypeDescriptor)); - return targetArray; - } else { - Enum[] source = (Enum[]) object; - - return Values.value(Arrays.stream(source) - .map(e -> delegate.convert(e, Objects.requireNonNull(sourceType.getElementTypeDescriptor()), TypeDescriptor.valueOf(Value.class))) - .toArray()); - } - } - } - static Float asFloat(Value value) { return Float.parseFloat(value.asString()); } @@ -363,8 +294,7 @@ final class AdditionalTypes { return Values.value(aFloat.toString()); } - @Nullable - static Locale asLocale(Value value) { + @Nullable static Locale asLocale(Value value) { return StringUtils.parseLocale(value.asString()); } @@ -492,5 +422,112 @@ final class AdditionalTypes { return Values.value(values); } - private AdditionalTypes() {} + @ReadingConverter + @WritingConverter + static final class EnumConverter implements GenericConverter { + + private final Set convertibleTypes; + + EnumConverter() { + Set tmp = new HashSet<>(); + tmp.add(new ConvertiblePair(Value.class, Enum.class)); + tmp.add(new ConvertiblePair(Enum.class, Value.class)); + this.convertibleTypes = Collections.unmodifiableSet(tmp); + } + + @Override + public Set getConvertibleTypes() { + return this.convertibleTypes; + } + + @SuppressWarnings({ "raw", "unchecked" }) // Due to dynamic enum retrieval + @Override + @Nullable public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source == null) { + return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; + } + + if (Value.class.isAssignableFrom(sourceType.getType())) { + return Enum.valueOf((Class) targetType.getType(), ((Value) source).asString()); + } + else { + return Values.value(((Enum) source).name()); + } + } + + } + + /** + * This is a workaround for the fact that Spring Data Commons requires + * {@link GenericConverter generic converters} to have a non-null convertible pair + * since 2.3. Without it, they get filtered out and thus not registered in a + * conversion service. We do this as an afterthought in + * {@link Neo4jConversions#registerConvertersIn(ConverterRegistry)}. + *

+ * This class uses is a {@link GenericConverter} without a concrete pair of + * convertible types. By making it implement {@link ConditionalConverter} it works + * with Springs conversion service out of the box. + */ + static final class EnumArrayConverter implements GenericConverter, ConditionalConverter { + + private final EnumConverter delegate; + + EnumArrayConverter() { + this.delegate = new EnumConverter(); + } + + private static boolean describesSupportedEnumVariant(TypeDescriptor typeDescriptor) { + var elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor(); + return typeDescriptor.isArray() && elementTypeDescriptor != null + && Enum.class.isAssignableFrom(elementTypeDescriptor.getType()); + } + + @Override + @Nullable public Set getConvertibleTypes() { + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (Value.class.isAssignableFrom(sourceType.getType())) { + return describesSupportedEnumVariant(targetType); + } + else if (Value.class.isAssignableFrom(targetType.getType())) { + return describesSupportedEnumVariant(sourceType); + } + else { + return false; + } + } + + @Override + @Nullable public Object convert(@Nullable Object object, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (object == null) { + return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null; + } + + if (Value.class.isAssignableFrom(sourceType.getType())) { + Value source = (Value) object; + + TypeDescriptor elementTypeDescriptor = Objects.requireNonNull(targetType.getElementTypeDescriptor()); + Object[] targetArray = (Object[]) Array.newInstance(elementTypeDescriptor.getType(), source.size()); + + Arrays.setAll(targetArray, i -> this.delegate.convert(source.get(i), + TypeDescriptor.valueOf(Value.class), elementTypeDescriptor)); + return targetArray; + } + else { + Enum[] source = (Enum[]) object; + + return Values.value(Arrays.stream(source) + .map(e -> this.delegate.convert(e, Objects.requireNonNull(sourceType.getElementTypeDescriptor()), + TypeDescriptor.valueOf(Value.class))) + .toArray()); + } + } + + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java b/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java index a34f825ed..e585c0b54 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/ConvertWith.java @@ -28,23 +28,30 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; /** - * This annotation can be used to define either custom conversions for single attributes by specifying a custom - * {@link Neo4jPersistentPropertyConverter} and if needed, a custom factory to create that converter or the annotation - * can be used to build custom meta-annotated annotations like {@code @org.springframework.data.neo4j.core.support.DateLong}. + * This annotation can be used to define either custom conversions for single attributes + * by specifying a custom {@link Neo4jPersistentPropertyConverter} and if needed, a custom + * factory to create that converter or the annotation can be used to build custom + * meta-annotated annotations like + * {@code @org.springframework.data.neo4j.core.support.DateLong}. * - *

Custom conversions are applied to both attributes of entities and parameters of repository methods that map to those - * attributes (which does apply to all derived queries and queries by example but not to string based queries). + *

+ * Custom conversions are applied to both attributes of entities and parameters of + * repository methods that map to those attributes (which does apply to all derived + * queries and queries by example but not to string based queries). * - *

Converters that have a default constructor don't need a dedicated factory. A dedicated factory will be provided with - * either this annotation and its values or with the meta annotated annotation, including all configuration - * available. + *

+ * Converters that have a default constructor don't need a dedicated factory. A dedicated + * factory will be provided with either this annotation and its values or with the meta + * annotated annotation, including all configuration available. * - *

In case {@link ConvertWith#converterRef()} is set to a non {@literal null} and non-empty value, the mapping context - * will try to lookup a bean under the given name of type {@link Neo4jPersistentPropertyConverter} in the application context. - * If no such bean is found an exception will be thrown. This attribute has precedence over {@link ConvertWith#converter()}. + *

+ * In case {@link ConvertWith#converterRef()} is set to a non {@literal null} and + * non-empty value, the mapping context will try to lookup a bean under the given name of + * type {@link Neo4jPersistentPropertyConverter} in the application context. If no such + * bean is found an exception will be thrown. This attribute has precedence over + * {@link ConvertWith#converter()}. * * @author Michael J. Simons - * @soundtrack Antilopen Gang - Abwasser * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) @@ -55,17 +62,24 @@ import org.neo4j.driver.Values; public @interface ConvertWith { /** - * @return The converter to instantiated for converting attributes to properties and vice versa. + * The converter to instantiated for converting attributes to properties and vice + * versa. + * @return The converter to instantiated for converting attributes to properties and + * vice versa */ Class> converter() default UnsetConverter.class; /** - * @return An alternative to {@link #converter()}, for all the scenarios in which constructing a converter is more effort than a constructor call. + * Allows to specify a factory for creating converters. + * @return An alternative to {@link #converter()}, for all the scenarios in which + * constructing a converter is more effort than a constructor call. */ Class converterFactory() default DefaultNeo4jPersistentPropertyConverterFactory.class; /** - * @return An optional reference to a bean to be used as converter, must implement {@link Neo4jPersistentPropertyConverter}. + * Reference to a Spring bean to be used as converter. + * @return An optional reference to a bean to be used as converter, must implement + * {@link Neo4jPersistentPropertyConverter}. */ String converterRef() default ""; @@ -74,14 +88,16 @@ public @interface ConvertWith { */ final class UnsetConverter implements Neo4jPersistentPropertyConverter { - @Override public Value write(@Nullable Object source) { + @Override + public Value write(@Nullable Object source) { return Values.NULL; } @Override - @Nullable - public Object read(@Nullable Value source) { + @Nullable public Object read(@Nullable Value source) { return null; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/CypherTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/CypherTypes.java index 793d93977..6afe0b8d0 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/CypherTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/CypherTypes.java @@ -29,11 +29,13 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.IsoDuration; import org.neo4j.driver.types.Point; + import org.springframework.data.convert.ConverterBuilder; /** - * Conversions for all known Cypher types, directly supported by the driver. See - * Working with Cypher values. + * Conversions for all known Cypher types, directly supported by the driver. See Working + * with Cypher values. * * @author Michael J. Simons * @since 6.0 @@ -57,15 +59,21 @@ final class CypherTypes { hlp.add(ConverterBuilder.reading(Value.class, byte[].class, Value::asByteArray).andWriting(Values::value)); hlp.add(ConverterBuilder.reading(Value.class, LocalDate.class, Value::asLocalDate).andWriting(Values::value)); hlp.add(ConverterBuilder.reading(Value.class, OffsetTime.class, Value::asOffsetTime).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, OffsetDateTime.class, Value::asOffsetDateTime).andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, OffsetDateTime.class, Value::asOffsetDateTime) + .andWriting(Values::value)); hlp.add(ConverterBuilder.reading(Value.class, LocalTime.class, Value::asLocalTime).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, ZonedDateTime.class, Value::asZonedDateTime).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, LocalDateTime.class, Value::asLocalDateTime).andWriting(Values::value)); - hlp.add(ConverterBuilder.reading(Value.class, IsoDuration.class, Value::asIsoDuration).andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, ZonedDateTime.class, Value::asZonedDateTime) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, LocalDateTime.class, Value::asLocalDateTime) + .andWriting(Values::value)); + hlp.add(ConverterBuilder.reading(Value.class, IsoDuration.class, Value::asIsoDuration) + .andWriting(Values::value)); hlp.add(ConverterBuilder.reading(Value.class, Point.class, Value::asPoint).andWriting(Values::value)); CONVERTERS = Collections.unmodifiableList(hlp); } - private CypherTypes() {} + private CypherTypes() { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java b/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java index c6e54c98f..649876b6e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/DefaultNeo4jPersistentPropertyConverterFactory.java @@ -21,8 +21,10 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.util.StringUtils; /** + * Default converter for {@link Neo4jPersistentProperty Neo4j specific persistent + * properties}. + * * @author Michael J. Simons - * @soundtrack Metallica - S&M2 * @since 6.0 */ final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory { @@ -41,12 +43,12 @@ final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersi ConvertWith config = persistentProperty.getRequiredAnnotation(ConvertWith.class); if (StringUtils.hasText(config.converterRef())) { - if (beanFactory == null) { + if (this.beanFactory == null) { throw new IllegalStateException( "The default converter factory has been configured without a bean factory and cannot use a converter from the application context"); } - return beanFactory.getBean(config.converterRef(), Neo4jPersistentPropertyConverter.class); + return this.beanFactory.getBean(config.converterRef(), Neo4jPersistentPropertyConverter.class); } if (config.converter() == ConvertWith.UnsetConverter.class) { @@ -56,4 +58,5 @@ final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersi return BeanUtils.instantiateClass(config.converter()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java index 8b526e7a9..a7f6814e7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversionService.java @@ -18,69 +18,78 @@ package org.springframework.data.neo4j.core.convert; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; + import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.data.util.TypeInformation; /** - * This service orchestrates a standard Spring conversion service with {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} registered. - * It provides simple delegating functions that allow for an override of the converter being used. + * This service orchestrates a standard Spring conversion service with + * {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} registered. It + * provides simple delegating functions that allow for an override of the converter being + * used. * * @author Michael J. Simons - * @soundtrack Die Ärzte - Die Nacht der Dämonen * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface Neo4jConversionService { /** - * Delegates to the underlying service, without the possibility to run a custom conversion. - * - * @param source The source to be converted - * @param targetType The target type - * @param The type to be returned - * @return The converted value + * Delegates to the underlying service, without the possibility to run a custom + * conversion. + * @param source the source to be converted + * @param targetType the target type + * @param the type to be returned + * @return the converted value */ - @Nullable - T convert(Object source, Class targetType); + @Nullable T convert(Object source, Class targetType); /** - * Returns whether we have a custom conversion registered to read {@code sourceType} into a native type. The returned - * type might be a subclass of the given expected type though. - * + * Returns whether we have a custom conversion registered to read {@code sourceType} + * into a native type. The returned type might be a subclass of the given expected + * type though. * @param sourceType must not be {@literal null} - * @return True if a custom write target exists. + * @return true if a custom write target exists. * @see org.springframework.data.convert.CustomConversions#hasCustomWriteTarget(Class) */ boolean hasCustomWriteTarget(Class sourceType); /** - * Reads a {@link Value} returned by the driver and converts it into a {@link Neo4jSimpleTypes simple type} supported - * by Neo4j SDN. If the value cannot be converted, a {@link TypeMismatchDataAccessException} will be thrown, it's - * cause indicating the failed conversion. + * Reads a {@link Value} returned by the driver and converts it into a + * {@link Neo4jSimpleTypes simple type} supported by Neo4j SDN. If the value cannot be + * converted, a {@link TypeMismatchDataAccessException} will be thrown, it's cause + * indicating the failed conversion. * - *

The returned object is generic as this method will take create target collections in case the incoming value describes a collection. - * - * @param source The value to be read, may be null. - * @param targetType The type information describing the target type. - * @param conversionOverride An optional conversion override. - * @return A simple type or null, if the value was {@literal null} or {@link org.neo4j.driver.Values#NULL}. - * @throws TypeMismatchDataAccessException In case the value cannot be converted to the target type + *

+ * The returned object is generic as this method will take create target collections + * in case the incoming value describes a collection. + * @param source the value to be read, may be null. + * @param targetType the type information describing the target type. + * @param conversionOverride an optional conversion override. + * @return a simple type or null, if the value was {@literal null} or + * {@link org.neo4j.driver.Values#NULL}. + * @throws TypeMismatchDataAccessException in case the value cannot be converted to + * the target type */ - @Nullable - Object readValue(@Nullable Value source, TypeInformation targetType, @Nullable Neo4jPersistentPropertyConverter conversionOverride); + @Nullable Object readValue(@Nullable Value source, TypeInformation targetType, + @Nullable Neo4jPersistentPropertyConverter conversionOverride); /** * Converts an {@link Object} to a driver's value object. - * - * @param value The value to get written, may be null. - * @param sourceType The type information describing the target type. - * @return A driver compatible value object. + * @param value the value to get written, may be null. + * @param sourceType the type information describing the target type. + * @param conversionOverride a conversion overriding the default + * @return a driver compatible value object. */ - Value writeValue(@Nullable Object value, TypeInformation sourceType, @Nullable Neo4jPersistentPropertyConverter conversionOverride); + Value writeValue(@Nullable Object value, TypeInformation sourceType, + @Nullable Neo4jPersistentPropertyConverter conversionOverride); /** - * @param type A type that should be checked whether it's simple or not. - * @return True if {@code type} is a simple type, according to {@link Neo4jSimpleTypes} and the registered converters. + * Return {@literal true} if the given class represents a Neo4j simple type. + * @param type a type that should be checked whether it's simple or not + * @return true if {@code type} is a simple type, according to + * {@link Neo4jSimpleTypes} and the registered converters. */ boolean isSimpleType(Class type); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversions.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversions.java index 9c4b3c3b6..df5d1deed 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversions.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jConversions.java @@ -21,18 +21,22 @@ import java.util.Collections; import java.util.List; import org.apiguardian.api.API; + import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.data.convert.CustomConversions; /** + * Manages all build-in Neo4j conversions: Cypher types, some additional types and the + * shared set of Spring Data and Neo4j spatial types. + * * @author Michael J. Simons - * @soundtrack The Kleptones - A Night At The Hip-Hopera * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public final class Neo4jConversions extends CustomConversions { private static final StoreConversions STORE_CONVERSIONS; + private static final List STORE_CONVERTERS; static { @@ -56,7 +60,6 @@ public final class Neo4jConversions extends CustomConversions { /** * Creates a new {@link CustomConversions} instance registering the given converters. - * * @param converters must not be {@literal null}. */ public Neo4jConversions(Collection converters) { @@ -68,4 +71,5 @@ public final class Neo4jConversions extends CustomConversions { super.registerConvertersIn(conversionService); conversionService.addConverter(new AdditionalTypes.EnumArrayConverter()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java index aec562952..5c1e7be82 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java @@ -20,26 +20,32 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; /** - * This interface represents a pair of methods capable of converting values of type {@code T} to and from {@link Value values}. + * This interface represents a pair of methods capable of converting values of type + * {@code T} to and from {@link Value values}. * + * @param the type of the property to convert (the type of the actual attribute). * @author Michael J. Simons - * @param The type of the property to convert (the type of the actual attribute). - * @soundtrack Antilopen Gang - Adrenochrom * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface Neo4jPersistentPropertyConverter { /** - * @param source The value to store. We might pass {@literal null}, if your converter is not able to handle that, - * this is ok, we do handle {@link NullPointerException null pointer exceptions} - * @return The converted value, never null. To represent {@literal null}, use {@link org.neo4j.driver.Values#NULL} + * Writes a property to a Neo4j value. + * @param source the value to store. We might pass {@literal null}, if your converter + * is not able to handle that, this is ok, we do handle {@link NullPointerException + * null pointer exceptions} + * @return the converted value, never null. To represent {@literal null}, use + * {@link org.neo4j.driver.Values#NULL} */ Value write(@Nullable T source); /** - * @param source The value to read, never null or {@link org.neo4j.driver.Values#NULL} - * @return The converted value, maybe null if {@code source} was equals to {@link org.neo4j.driver.Values#NULL}. + * Reads a property from a Neo4j value. + * @param source the value to read, never null or {@link org.neo4j.driver.Values#NULL} + * @return the converted value, maybe null if {@code source} was equals to + * {@link org.neo4j.driver.Values#NULL}. */ @Nullable T read(@Nullable Value source); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverterFactory.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverterFactory.java index 69d8bbeaa..0803fede4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverterFactory.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverterFactory.java @@ -18,26 +18,33 @@ package org.springframework.data.neo4j.core.convert; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; /** - * This interface needs to be implemented to provide custom configuration for a {@link Neo4jPersistentPropertyConverter}. Use cases may - * be specific date formats or the like. The build method will receive the whole property. It is safe to assume that at - * least the {@link ConvertWith @ConvertWith} annotation is present on the property, either directly or meta-annotated. + * This interface needs to be implemented to provide custom configuration for a + * {@link Neo4jPersistentPropertyConverter}. Use cases may be specific date formats or the + * like. The build method will receive the whole property. It is safe to assume that at + * least the {@link ConvertWith @ConvertWith} annotation is present on the property, + * either directly or meta-annotated. * - *

Classes implementing this interface should have a default constructor. In case they provide a constructor asking for - * an instance of {@link Neo4jConversionService}, such service is provided. This allows for conversions delegating part - * of the conversion. + *

+ * Classes implementing this interface should have a default constructor. In case they + * provide a constructor asking for an instance of {@link Neo4jConversionService}, such + * service is provided. This allows for conversions delegating part of the conversion. * - *

In same cases a factory might be interested in having access to a {@link org.springframework.beans.factory.BeanFactory}. - * In case SDN can provide it, it will prefer such a constructor to the default one or the one taken a {@link Neo4jConversionService}. + *

+ * In same cases a factory might be interested in having access to a + * {@link org.springframework.beans.factory.BeanFactory}. In case SDN can provide it, it + * will prefer such a constructor to the default one or the one taken a + * {@link Neo4jConversionService}. * * @author Michael J. Simons - * @soundtrack Antilopen Gang - Abwasser * @since 6.0 */ public interface Neo4jPersistentPropertyConverterFactory { /** - * @param persistentProperty The property for which the converter should be build. - * @return The new or existing converter + * Finds fitting {@link Neo4jPersistentPropertyConverter} for a given property. + * @param persistentProperty the property for which the converter should be built + * @return the new or existing converter */ Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java index 1e8ad756b..2130d2b14 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java @@ -22,37 +22,40 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; /** - * You need to provide an implementation of this interface in case you want to store a property of an entity as separate - * properties on a node. The entity needs to be decomposed into a map and composed from a map for that purpose. + * You need to provide an implementation of this interface in case you want to store a + * property of an entity as separate properties on a node. The entity needs to be + * decomposed into a map and composed from a map for that purpose. * - *

The calling mechanism will take care of adding and removing configured prefixes and transforming keys and values into - * something that Neo4j can understand. + *

+ * The calling mechanism will take care of adding and removing configured prefixes and + * transforming keys and values into something that Neo4j can understand. * + * @param the type of the keys (Only Strings and Enums are supported). + * @param

the type of the property. * @author Michael J. Simons - * @param The type of the keys (Only Strings and Enums are supported). - * @param

The type of the property. - * @soundtrack Metallica - Helping Hands… Live & Acoustic At The Masonic * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface Neo4jPersistentPropertyToMapConverter { /** - * Decomposes an object into a map. A conversion service is provided in case delegation is needed. - * - * @param property The source property - * @param neo4jConversionService The conversion service to delegate to if necessary - * @return The decomposed object. + * Decomposes an object into a map. A conversion service is provided in case + * delegation is needed. + * @param property the source property + * @param neo4jConversionService the conversion service to delegate to if necessary + * @return the decomposed object. */ Map decompose(@Nullable P property, Neo4jConversionService neo4jConversionService); /** - * Composes the object back from the map. The map contains the raw driver values, as SDN cannot know how you want to - * handle them. Therefore, the conversion service to convert driver values is provided. - * - * @param source The source map - * @param neo4jConversionService The conversion service in case you want to delegate the work for some values in the map - * @return The composed object. + * Composes the object back from the map. The map contains the raw driver values, as + * SDN cannot know how you want to handle them. Therefore, the conversion service to + * convert driver values is provided. + * @param source the source map + * @param neo4jConversionService the conversion service in case you want to delegate + * the work for some values in the map + * @return the composed object. */ P compose(Map source, Neo4jConversionService neo4jConversionService); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jSimpleTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jSimpleTypes.java index 00c9e60de..901e3753c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jSimpleTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/Neo4jSimpleTypes.java @@ -33,6 +33,7 @@ import org.apiguardian.api.API; import org.neo4j.driver.Value; import org.neo4j.driver.types.IsoDuration; import org.neo4j.driver.types.Point; + import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.neo4j.types.CartesianPoint2d; import org.springframework.data.neo4j.types.CartesianPoint3d; @@ -40,13 +41,14 @@ import org.springframework.data.neo4j.types.GeographicPoint2d; import org.springframework.data.neo4j.types.GeographicPoint3d; /** - * A list of Neo4j simple types: All attributes that can be mapped to a property. Some special logic has to be applied - * for domain attributes of the collection types {@link java.util.List} and {@link java.util.Map}. Those can be mapped - * to simple properties as well as to relationships to other things. + * A list of Neo4j simple types: All attributes that can be mapped to a property. Some + * special logic has to be applied for domain attributes of the collection types + * {@link java.util.List} and {@link java.util.Map}. Those can be mapped to simple + * properties as well as to relationships to other things. *

- * The Java driver itself has a good overview of the supported types: - * The Cypher type - * system. + * The Java driver itself has a good overview of the supported types: The + * Cypher type system. * * @author Michael J. Simons * @since 6.0 @@ -87,9 +89,12 @@ public final class Neo4jSimpleTypes { } /** - * The simple types we support plus all the simple types recognized by Spring. Not taking custom conversions into account. + * The simple types we support plus all the simple types recognized by Spring. Not + * taking custom conversions into account. */ public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true); - private Neo4jSimpleTypes() {} + private Neo4jSimpleTypes() { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/SpatialTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/SpatialTypes.java index 5daf4aaf8..996402156 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/SpatialTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/SpatialTypes.java @@ -21,6 +21,7 @@ import java.util.List; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.geo.Point; import org.springframework.data.neo4j.types.CartesianPoint2d; @@ -35,18 +36,20 @@ import org.springframework.util.Assert; /** * Mapping of spatial types. *

- * This replicates the behaviour of SDN+OGM. Spring Data Commons geographic points are x/y based and usually treat x/y - * as lat/long. + * This replicates the behaviour of SDN+OGM. Spring Data Commons geographic points are x/y + * based and usually treat x/y as lat/long. *

- * Neo4j however stores x/y as long/lat when used with an Srid of 4326 or 4979 (those are geographic points). We take - * this into account with our dedicated spatial types which can be used alternatively. + * Neo4j however stores x/y as long/lat when used with an Srid of 4326 or 4979 (those are + * geographic points). We take this into account with our dedicated spatial types which + * can be used alternatively. *

- * However, when converting an Spring Data Commons point to the internal value, you'll notice that we store y as x and - * vice versa. This is intentionally. We use a hardcoded WGS-84 Srid during storage, thus you'll get back your x as - * latitude, y as longitude, as described above. + * However, when converting an Spring Data Commons point to the internal value, you'll + * notice that we store y as x and vice versa. This is intentionally. We use a hardcoded + * WGS-84 Srid during storage, thus you'll get back your x as latitude, y as longitude, as + * described above. *

- * The biggest degree of freedom will come from using an attribute of type {@link org.neo4j.driver.types.Point} - * directly. This will be passed on as is. + * The biggest degree of freedom will come from using an attribute of type + * {@link org.neo4j.driver.types.Point} directly. This will be passed on as is. * * @author Michael J. Simons * @since 6.0 @@ -58,14 +61,20 @@ final class SpatialTypes { static { List hlp = new ArrayList<>(); - hlp.add(ConverterBuilder.reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint).andWriting(SpatialTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, Point[].class, SpatialTypes::asPointArray).andWriting(SpatialTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint) + .andWriting(SpatialTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Point[].class, SpatialTypes::asPointArray) + .andWriting(SpatialTypes::value)); - hlp.add(ConverterBuilder.reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint).andWriting(SpatialTypes::value)); + hlp.add(ConverterBuilder.reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint) + .andWriting(SpatialTypes::value)); CONVERTERS = Collections.unmodifiableList(hlp); } + private SpatialTypes() { + } + static Neo4jPoint asNeo4jPoint(Value value) { org.neo4j.driver.types.Point point = value.asPoint(); @@ -79,16 +88,20 @@ final class SpatialTypes { if (object instanceof CartesianPoint2d) { CartesianPoint2d point = (CartesianPoint2d) object; return Values.point(point.getSrid(), point.getX(), point.getY()); - } else if (object instanceof CartesianPoint3d) { + } + else if (object instanceof CartesianPoint3d) { CartesianPoint3d point = (CartesianPoint3d) object; return Values.point(point.getSrid(), point.getX(), point.getY(), point.getZ()); - } else if (object instanceof GeographicPoint2d) { + } + else if (object instanceof GeographicPoint2d) { GeographicPoint2d point = (GeographicPoint2d) object; return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude()); - } else if (object instanceof GeographicPoint3d) { + } + else if (object instanceof GeographicPoint3d) { GeographicPoint3d point = (GeographicPoint3d) object; return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude(), point.getHeight()); - } else { + } + else { throw new IllegalArgumentException("Unsupported point implementation: " + object.getClass()); } } @@ -128,5 +141,4 @@ final class SpatialTypes { return Values.value(values); } - private SpatialTypes() {} } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapter.java b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapter.java index 135fafe97..995d527e4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapter.java @@ -24,32 +24,41 @@ import java.util.function.BiFunction; import java.util.function.Function; /** - * This adapter maps a Driver or embedded based {@link TemporalAmount} to a valid Java temporal amount. It tries to be - * as specific as possible: If the amount can be reliable mapped to a {@link Period}, it returns a period. If only - * fields are present that are no estimated time unites, then it returns a {@link Duration}.
+ * This adapter maps a Driver or embedded based {@link TemporalAmount} to a valid Java + * temporal amount. It tries to be as specific as possible: If the amount can be reliable + * mapped to a {@link Period}, it returns a period. If only fields are present that are no + * estimated time unites, then it returns a {@link Duration}.
*
* In cases a user has used Cypher and its duration() function, i.e. like so - * CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s a duration object has been - * created that cannot be represented by either a {@link Period} or {@link Duration}. The user has to map it to a plain + * CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s + * a duration object has been created that cannot be represented by either a + * {@link Period} or {@link Duration}. The user has to map it to a plain * {@link TemporalAmount} in these cases.
- * The Java Driver uses a org.neo4j.driver.v1.types.IsoDuration, embedded uses - * org.neo4j.values.storable.DurationValue for representing a temporal amount, but in the end, they can be - * treated the same. However be aware that the temporal amount returned in that case may not be equal to the other one, - * only represents the same amount after normalization. + * The Java Driver uses a org.neo4j.driver.v1.types.IsoDuration, embedded + * uses org.neo4j.values.storable.DurationValue for representing a temporal + * amount, but in the end, they can be treated the same. However be aware that the + * temporal amount returned in that case may not be equal to the other one, only + * represents the same amount after normalization. * * @author Michael J. Simons */ final class TemporalAmountAdapter implements Function { private static final int PERIOD_MASK = 0b11100; + private static final int DURATION_MASK = 0b00011; + private static final TemporalUnit[] SUPPORTED_UNITS = { ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.DAYS, ChronoUnit.SECONDS, ChronoUnit.NANOS }; private static final short FIELD_YEAR = 0; + private static final short FIELD_MONTH = 1; + private static final short FIELD_DAY = 2; + private static final short FIELD_SECONDS = 3; + private static final short FIELD_NANOS = 4; private static final BiFunction TEMPORAL_UNIT_EXTRACTOR = (d, u) -> { @@ -59,6 +68,14 @@ final class TemporalAmountAdapter implements Function 0; + } + + private static boolean couldBeDuration(int type) { + return (DURATION_MASK & type) > 0; + } + @Override public TemporalAmount apply(TemporalAmount internalTemporalAmountRepresentation) { @@ -74,18 +91,13 @@ final class TemporalAmountAdapter implements Function 0; - } - - private static boolean couldBeDuration(int type) { - return (DURATION_MASK & type) > 0; - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java index 2e0064c48..c70040b78 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/TemporalAmountConverter.java @@ -27,58 +27,56 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.IsoDuration; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; /** - * This generic converter has been introduced to augment the {@link TemporalAmountAdapter} with the type information passed - * to a generic converter to make some educated guesses whether an {@link org.neo4j.driver.types.IsoDuration} of {@literal 0} - * should be possibly treated as {@link java.time.Period} or {@link java.time.Duration}. + * This generic converter has been introduced to augment the {@link TemporalAmountAdapter} + * with the type information passed to a generic converter to make some educated guesses + * whether an {@link org.neo4j.driver.types.IsoDuration} of {@literal 0} should be + * possibly treated as {@link java.time.Period} or {@link java.time.Duration}. * * @author Michael J. Simons - * @soundtrack Motörhead - Bomber */ final class TemporalAmountConverter implements GenericConverter { private final TemporalAmountAdapter adapter = new TemporalAmountAdapter(); - private final Set convertibleTypes = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList( - new ConvertiblePair(Value.class, TemporalAmount.class), - new ConvertiblePair(TemporalAmount.class, Value.class) - ))); - @Override - public Set getConvertibleTypes() { - return convertibleTypes; + private final Set convertibleTypes = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(new ConvertiblePair(Value.class, TemporalAmount.class), + new ConvertiblePair(TemporalAmount.class, Value.class)))); + + private static boolean isZero(IsoDuration isoDuration) { + + return isoDuration.months() == 0L && isoDuration.days() == 0L && isoDuration.seconds() == 0L + && isoDuration.nanoseconds() == 0L; } @Override - @Nullable - public Object convert(@Nullable Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { + public Set getConvertibleTypes() { + return this.convertibleTypes; + } + + @Override + @Nullable public Object convert(@Nullable Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { if (TemporalAmount.class.isAssignableFrom(sourceType.getType())) { return Values.value(value); } - Object convertedValue = value == null || value == Values.NULL ? null : adapter.apply(((Value) value).asIsoDuration()); + Object convertedValue = (value == null || value == Values.NULL) ? null + : this.adapter.apply(((Value) value).asIsoDuration()); if (convertedValue instanceof IsoDuration && isZero((IsoDuration) convertedValue)) { if (Period.class.isAssignableFrom(targetType.getType())) { return Period.of(0, 0, 0); - } else if (Duration.class.isAssignableFrom(targetType.getType())) { + } + else if (Duration.class.isAssignableFrom(targetType.getType())) { return Duration.ZERO; } } return convertedValue; } - /** - * @param isoDuration The duration to check whether it's {@literal 0} or not. - * @return True if there are only temporal units in that duration with a value of {@literal 0}. - */ - private static boolean isZero(IsoDuration isoDuration) { - - return isoDuration.months() == 0L && isoDuration.days() == 0L && - isoDuration.seconds() == 0L && isoDuration.nanoseconds() == 0L; - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java b/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java index fbad27a4c..90365ae2c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/package-info.java @@ -1,8 +1,22 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - Provides a set of simples types that SDN supports. The `Neo4jConversions` allows bringing in additional, custom - converters. - * + * Provides a set of simples types that SDN supports. The + * `Neo4jConversions` allows bringing in additional, custom converters. */ @NullMarked package org.springframework.data.neo4j.core.convert; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/AssociationHandlerSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/AssociationHandlerSupport.java index d8cd74b5b..336e9b824 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/AssociationHandlerSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/AssociationHandlerSupport.java @@ -19,11 +19,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apiguardian.api.API; + import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * Warning Internal API, might change without further notice, even in patch releases. + * Warning Internal API, might change without further notice, even in + * patch releases. *

* This class removes {@link TargetNode @TargetNode} properties again from associations. * @@ -33,11 +35,7 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @API(status = API.Status.INTERNAL, since = "6.3") public final class AssociationHandlerSupport { - private final static Map, AssociationHandlerSupport> CACHE = new ConcurrentHashMap<>(); - - public static AssociationHandlerSupport of(Neo4jPersistentEntity entity) { - return CACHE.computeIfAbsent(entity, AssociationHandlerSupport::new); - } + private static final Map, AssociationHandlerSupport> CACHE = new ConcurrentHashMap<>(); private final Neo4jPersistentEntity entity; @@ -45,12 +43,17 @@ public final class AssociationHandlerSupport { this.entity = entity; } + public static AssociationHandlerSupport of(Neo4jPersistentEntity entity) { + return CACHE.computeIfAbsent(entity, AssociationHandlerSupport::new); + } + public Neo4jPersistentEntity doWithAssociations(AssociationHandler handler) { - entity.doWithAssociations((AssociationHandler) association -> { + this.entity.doWithAssociations((AssociationHandler) association -> { if (!association.getInverse().isAnnotationPresent(TargetNode.class)) { handler.doWithAssociation(association); } }); - return entity; + return this.entity; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java index a2dd80575..a7a36ce0e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java @@ -15,74 +15,155 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.function.Function; + import org.apiguardian.api.API; import org.apiguardian.api.API.Status; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SymbolicName; + import org.springframework.util.StringUtils; -import java.util.function.Function; - /** - * A pool of constants used in our Cypher generation. These constants may change without further notice and are meant - * for internal use only. + * A pool of constants used in our Cypher generation. These constants may change without + * further notice and are meant for internal use only. * * @author Michael J. Simons - * @soundtrack Milky Chance - Sadnecessary * @since 6.0 */ @API(status = Status.EXPERIMENTAL, since = "6.0") public final class Constants { - public static final Function, SymbolicName> NAME_OF_TYPED_ROOT_NODE = - (nodeDescription) -> nodeDescription != null - ? Cypher.name(StringUtils.uncapitalize(nodeDescription.getUnderlyingClass().getSimpleName())) - : Cypher.name("n"); + /** + * A function for deriving a name for the root node of a query. + */ + public static final Function, SymbolicName> NAME_OF_TYPED_ROOT_NODE = ( + nodeDescription) -> (nodeDescription != null) + ? Cypher.name(StringUtils.uncapitalize(nodeDescription.getUnderlyingClass().getSimpleName())) + : Cypher.name("n"); + /** + * A generic name for an untyped root node. + */ public static final SymbolicName NAME_OF_ROOT_NODE = NAME_OF_TYPED_ROOT_NODE.apply(null); + /** + * The name of the property SDN uses to transport the internal Neo4j entity id. + */ public static final String NAME_OF_INTERNAL_ID = "__internalNeo4jId__"; + + /** + * The name of the property SDN uses to transport the Neo4j element id. + */ public static final String NAME_OF_ELEMENT_ID = "__elementId__"; + /** + * The name of a property SDN might insert to guarantee a stable sort of records. + */ public static final String NAME_OF_ADDITIONAL_SORT = "__stable_uniq_sort__"; /** * Indicates the list of dynamic labels. */ public static final String NAME_OF_LABELS = "__nodeLabels__"; + /** * Indicates the list of all labels. */ public static final String NAME_OF_ALL_LABELS = "__labels__"; - public static final String NAME_OF_IDS = "__ids__"; - public static final String NAME_OF_ID = "__id__"; - public static final String NAME_OF_VERSION_PARAM = "__version__"; - public static final String NAME_OF_PROPERTIES_PARAM = "__properties__"; - public static final String NAME_OF_VECTOR_PROPERTY = "__vectorProperty__"; - public static final String NAME_OF_VECTOR_VALUE = "__vectorValue__"; + /** - * Indicates the parameter that contains the static labels which are required to correctly compute the difference - * in the list of dynamic labels when saving a node. + * The name of the property SDN uses to transport a set of ids. + */ + public static final String NAME_OF_IDS = "__ids__"; + + /** + * The name of the property SDN uses to transport an id. + */ + public static final String NAME_OF_ID = "__id__"; + + /** + * The name of the property SDN uses to transport the version of an entity. + */ + public static final String NAME_OF_VERSION_PARAM = "__version__"; + + /** + * The name of the property SDN uses to transport all projected properties. + */ + public static final String NAME_OF_PROPERTIES_PARAM = "__properties__"; + + /** + * The name of the property SDN uses to transport a vector property. + */ + public static final String NAME_OF_VECTOR_PROPERTY = "__vectorProperty__"; + + /** + * The name of the property SDN uses to transport the value of a vector property. + */ + public static final String NAME_OF_VECTOR_VALUE = "__vectorValue__"; + + /** + * Indicates the parameter that contains the static labels which are required to + * correctly compute the difference in the list of dynamic labels when saving a node. */ public static final String NAME_OF_STATIC_LABELS_PARAM = "__staticLabels__"; + + /** + * The name of the parameter SDN uses to pass a list of entities. + */ public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__"; + + /** + * The name of the parameter SDN uses to pass a list of relationships. + */ public static final String NAME_OF_RELATIONSHIP_LIST_PARAM = "__relationships__"; + + /** + * The name of the parameter SDN uses to pass a known relationship id. + */ public static final String NAME_OF_KNOWN_RELATIONSHIP_PARAM = "__knownRelationShipId__"; + + /** + * The name of the parameter SDN uses to pass a list of known relationship ids. + */ public static final String NAME_OF_KNOWN_RELATIONSHIPS_PARAM = "__knownRelationShipIds__"; + + /** + * The name of the parameter SDN uses to pass all properties. + */ public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__"; /** - * Optional property for relationship properties' simple class name to keep type info + * Optional property for relationship properties' simple class name to keep type info. */ public static final String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__"; + /** + * The name SDN uses for a synthesized root node. + */ public static final String NAME_OF_SYNTHESIZED_ROOT_NODE = "__sn__"; + + /** + * The name SDN uses for synthesized related nodes. + */ public static final String NAME_OF_SYNTHESIZED_RELATED_NODES = "__srn__"; + + /** + * The name SDN uses for synthesized relationships. + */ public static final String NAME_OF_SYNTHESIZED_RELATIONS = "__sr__"; + /** + * The name SDN uses for the parameter to pass the "from id". + */ public static final String FROM_ID_PARAMETER_NAME = "fromId"; + + /** + * The name SDN uses for the parameter to pass the "to id". + */ public static final String TO_ID_PARAMETER_NAME = "toId"; private Constants() { } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java index 9390c041e..6d93bbf96 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CreateRelationshipStatementHolder.java @@ -20,14 +20,16 @@ import java.util.Map; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; /** - * The {@link CreateRelationshipStatementHolder} holds the Cypher Statement to create a relationship as well as the optional - * properties that describe the relationship in case of more than a simple relationship. By holding the relationship - * creation cypher together with the properties, we can reuse the same logic in the {@link Neo4jTemplate} as well as in - * the {@link ReactiveNeo4jTemplate}. + * The {@link CreateRelationshipStatementHolder} holds the Cypher Statement to create a + * relationship as well as the optional properties that describe the relationship in case + * of more than a simple relationship. By holding the relationship creation cypher + * together with the properties, we can reuse the same logic in the {@link Neo4jTemplate} + * as well as in the {@link ReactiveNeo4jTemplate}. * * @author Philipp Tölle * @author Michael J. Simons @@ -37,6 +39,7 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; public final class CreateRelationshipStatementHolder { private final Statement statement; + private final Map properties; CreateRelationshipStatementHolder(Statement statement, Map properties) { @@ -45,11 +48,11 @@ public final class CreateRelationshipStatementHolder { } public Statement getStatement() { - return statement; + return this.statement; } public Map getProperties() { - return properties; + return this.properties; } public CreateRelationshipStatementHolder addProperty(String key, Object property) { @@ -57,4 +60,5 @@ public final class CreateRelationshipStatementHolder { newProperties.put(key, property); return new CreateRelationshipStatementHolder(this.statement, newProperties); } + } 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 ffa58a044..dbf0b0407 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 @@ -15,16 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.neo4j.cypherdsl.core.Cypher.anyNode; -import static org.neo4j.cypherdsl.core.Cypher.coalesce; -import static org.neo4j.cypherdsl.core.Cypher.collect; -import static org.neo4j.cypherdsl.core.Cypher.listBasedOn; -import static org.neo4j.cypherdsl.core.Cypher.literalOf; -import static org.neo4j.cypherdsl.core.Cypher.match; -import static org.neo4j.cypherdsl.core.Cypher.node; -import static org.neo4j.cypherdsl.core.Cypher.optionalMatch; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -62,57 +52,137 @@ import org.neo4j.cypherdsl.core.StatementBuilder.OngoingUpdate; import org.neo4j.cypherdsl.core.SymbolicName; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; + import org.springframework.data.domain.Sort; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.util.Assert; +import static org.neo4j.cypherdsl.core.Cypher.anyNode; +import static org.neo4j.cypherdsl.core.Cypher.coalesce; +import static org.neo4j.cypherdsl.core.Cypher.collect; +import static org.neo4j.cypherdsl.core.Cypher.listBasedOn; +import static org.neo4j.cypherdsl.core.Cypher.literalOf; +import static org.neo4j.cypherdsl.core.Cypher.match; +import static org.neo4j.cypherdsl.core.Cypher.node; +import static org.neo4j.cypherdsl.core.Cypher.optionalMatch; +import static org.neo4j.cypherdsl.core.Cypher.parameter; + /** - * A generator based on the schema defined by node and relationship descriptions. Most methods return renderable Cypher - * statements. + * A generator based on the schema defined by node and relationship descriptions. Most + * methods return renderable Cypher statements. * * @author Michael J. Simons * @author Gerrit Meier * @author Philipp Tölle - * @soundtrack Rammstein - Herzeleid * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") public enum CypherGenerator { + /** + * The sole instance of this generator. + */ INSTANCE; + private static final SymbolicName START_NODE_NAME = Cypher.name("startNode"); + + private static final SymbolicName END_NODE_NAME = Cypher.name("endNode"); + + private static final SymbolicName RELATIONSHIP_NAME = Cypher.name("relProps"); + + private static final Pattern LOOKS_LIKE_A_FUNCTION = Pattern.compile(".+\\(.*\\)"); + // keeping elementId/id function selection in one place within this class // default elementId function private Function elementIdOrIdFunction = named -> { if (named instanceof Node node) { return Cypher.elementId(node); - } else if (named instanceof Relationship relationship) { + } + else if (named instanceof Relationship relationship) { return Cypher.elementId(relationship); - } else { + } + else { throw new IllegalArgumentException("Unsupported CypherDSL type: " + named.getClass()); } }; + @SuppressWarnings("deprecation") + private static Function getNodeIdFunction(Neo4jPersistentEntity entity, + boolean canUseElementId) { + + Function startNodeIdFunction; + var idProperty = entity.getRequiredIdProperty(); + if (entity.isUsingInternalIds()) { + if (entity.isUsingDeprecatedInternalId() || !canUseElementId) { + startNodeIdFunction = Node::internalId; + } + else { + startNodeIdFunction = Cypher::elementId; + } + } + else { + startNodeIdFunction = node -> node.property(idProperty.getPropertyName()); + } + return startNodeIdFunction; + } + + @SuppressWarnings("deprecation") + private static Function getEndNodeIdFunction(Neo4jPersistentEntity entity, + boolean canUseElementId) { + + Function startNodeIdFunction; + if (entity == null) { + return Cypher::elementId; + } + if (!entity.isUsingDeprecatedInternalId() && canUseElementId) { + startNodeIdFunction = Cypher::elementId; + } + else { + startNodeIdFunction = Node::internalId; + } + return startNodeIdFunction; + } + + static Expression relId(Relationship r) { + return FunctionInvocation.create(() -> "id", r.getRequiredSymbolicName()); + } + + private static Function getRelationshipIdFunction( + RelationshipDescription relationshipDescription, boolean canUseElementId) { + + Function result = canUseElementId ? Cypher::elementId : CypherGenerator::relId; + if (relationshipDescription.hasRelationshipProperties()) { + Neo4jPersistentEntity entity = (Neo4jPersistentEntity) relationshipDescription + .getRelationshipPropertiesEntity(); + if ((entity != null && entity.isUsingDeprecatedInternalId()) || !canUseElementId) { + result = CypherGenerator::relId; + } + else { + result = Cypher::elementId; + } + } + return result; + } + + private static Condition conditionOrNoCondition(@Nullable Condition condition) { + return (condition != null) ? condition : Cypher.noCondition(); + } + /** * Set function to be used to query either elementId or id. - * * @param elementIdOrIdFunction new function to use. */ public void setElementIdOrIdFunction(Function elementIdOrIdFunction) { this.elementIdOrIdFunction = elementIdOrIdFunction; } - private static final SymbolicName START_NODE_NAME = Cypher.name("startNode"); - private static final SymbolicName END_NODE_NAME = Cypher.name("endNode"); - - private static final SymbolicName RELATIONSHIP_NAME = Cypher.name("relProps"); - private static final Pattern LOOKS_LIKE_A_FUNCTION = Pattern.compile(".+\\(.*\\)"); - /** - * @param nodeDescription The node description for which a match clause should be generated - * @return An ongoing match + * Prepares a match for a given entity. + * @param nodeDescription the node description for which a match clause should be + * generated + * @return an ongoing match * @see #prepareMatchOf(NodeDescription, Condition) */ public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription) { @@ -120,20 +190,23 @@ public enum CypherGenerator { } /** - * This will create a match statement that fits the given node description and may contain additional conditions. The - * {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to the given + * This will create a match statement that fits the given node description and may + * contain additional conditions. The {@code WITH} clause of this statement contains + * all nodes and relationships necessary to map a record to the given * {@code nodeDescription}. *

- * It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end. + * It is recommended to use {@link Cypher#asterisk()} to return everything from the + * query in the end. *

* The root node is guaranteed to have the symbolic name {@code n}. - * - * @param nodeDescription The node description for which a match clause should be generated - * @param condition Optional conditions to add - * @return An ongoing match + * @param nodeDescription the node description for which a match clause should be + * generated + * @param condition an optional conditions to add + * @return an ongoing match */ @SuppressWarnings("deprecation") - public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, @Nullable Condition condition) { + public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, + @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); @@ -142,28 +215,28 @@ public enum CypherGenerator { if (nodeDescription instanceof Neo4jPersistentEntity entity && entity.isUsingDeprecatedInternalId()) { expressions.add(rootNode.internalId().as(Constants.NAME_OF_INTERNAL_ID)); } - expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID)); + expressions.add(this.elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID)); - return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(IdentifiableElement[]::new)); + return match(rootNode).where(conditionOrNoCondition(condition)) + .with(expressions.toArray(IdentifiableElement[]::new)); } public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription nodeDescription, - List initialMatchOn, - @Nullable Condition condition) { + List initialMatchOn, @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn); List expressions = new ArrayList<>(); - expressions.add(Cypher.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); + expressions.add( + Cypher.collect(this.elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); - return match - .where(conditionOrNoCondition(condition)) - .with(expressions.toArray(IdentifiableElement[]::new)); + return match.where(conditionOrNoCondition(condition)).with(expressions.toArray(IdentifiableElement[]::new)); } public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription nodeDescription, - RelationshipDescription relationshipDescription, @Nullable List initialMatchOn, @Nullable Condition condition) { + RelationshipDescription relationshipDescription, @Nullable List initialMatchOn, + @Nullable Condition condition) { Node rootNode = createRootNode(nodeDescription); @@ -171,18 +244,20 @@ public enum CypherGenerator { Node targetNode = node(relationshipDescription.getTarget().getPrimaryLabel(), relationshipDescription.getTarget().getAdditionalLabels()) - .named(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); + .named(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); boolean dynamicRelationship = relationshipDescription.isDynamic(); - Class componentType = ((DefaultRelationshipDescription) relationshipDescription).getInverse().getComponentType(); + Class componentType = ((DefaultRelationshipDescription) relationshipDescription).getInverse() + .getComponentType(); List relationshipTypes = new ArrayList<>(); if (dynamicRelationship && componentType != null && componentType.isEnum()) { Arrays.stream(componentType.getEnumConstants()) - .forEach(constantName -> relationshipTypes.add(constantName.toString())); - } else if (!dynamicRelationship) { + .forEach(constantName -> relationshipTypes.add(constantName.toString())); + } + else if (!dynamicRelationship) { relationshipTypes.add(relationshipDescription.getType()); } - String[] types = relationshipTypes.toArray(new String[]{}); + String[] types = relationshipTypes.toArray(new String[] {}); Relationship relationship = switch (relationshipDescription.getDirection()) { case OUTGOING -> rootNode.relationshipTo(targetNode, types); @@ -191,14 +266,16 @@ public enum CypherGenerator { relationship = relationship.named(Constants.NAME_OF_SYNTHESIZED_RELATIONS); List expressions = new ArrayList<>(); - expressions.add(Cypher.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); - expressions.add(Cypher.collect(elementIdOrIdFunction.apply(targetNode)).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)); - expressions.add(Cypher.collect(elementIdOrIdFunction.apply(relationship)).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS)); + expressions.add( + Cypher.collect(this.elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE)); + expressions.add(Cypher.collect(this.elementIdOrIdFunction.apply(targetNode)) + .as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)); + expressions.add(Cypher.collect(this.elementIdOrIdFunction.apply(relationship)) + .as(Constants.NAME_OF_SYNTHESIZED_RELATIONS)); - return match - .where(conditionOrNoCondition(condition)) - .optionalMatch(relationship) - .with(expressions.toArray(IdentifiableElement[]::new)); + return match.where(conditionOrNoCondition(condition)) + .optionalMatch(relationship) + .with(expressions.toArray(IdentifiableElement[]::new)); } public Node createRootNode(NodeDescription nodeDescription) { @@ -208,18 +285,19 @@ public enum CypherGenerator { return node(primaryLabel, additionalLabels).named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); } - private OngoingReadingWithoutWhere prepareMatchOfRootNode( - Node rootNode, @Nullable List initialMatchOn - ) { + private OngoingReadingWithoutWhere prepareMatchOfRootNode(Node rootNode, + @Nullable List initialMatchOn) { OngoingReadingWithoutWhere match = null; if (initialMatchOn == null || initialMatchOn.isEmpty()) { match = Cypher.match(rootNode); - } else { + } + else { for (PatternElement patternElement : initialMatchOn) { if (match == null) { match = Cypher.match(patternElement); - } else { + } + else { match = match.match(patternElement); } } @@ -228,34 +306,41 @@ public enum CypherGenerator { } /** - * Creates a statement that returns all labels of a node that are not part of a list parameter named - * {@link Constants#NAME_OF_STATIC_LABELS_PARAM}. Those are the "dynamic labels" of a node as set through SDN. - * - * @param nodeDescription The node description for which the statement should be generated - * @return A statement having one parameter. + * Creates a statement that returns all labels of a node that are not part of a list + * parameter named {@link Constants#NAME_OF_STATIC_LABELS_PARAM}. Those are the + * "dynamic labels" of a node as set through SDN. + * @param nodeDescription the node description for which the statement should be + * generated + * @return a statement having one parameter * @since 6.0 */ public Statement createStatementReturningDynamicLabels(NodeDescription nodeDescription) { - IdDescription idDescription = Objects.requireNonNull(nodeDescription.getIdDescription(), "Cannot load specific nodes by id without a corresponding attribute"); + IdDescription idDescription = Objects.requireNonNull(nodeDescription.getIdDescription(), + "Cannot load specific nodes by id without a corresponding attribute"); final Node rootNode = createRootNode(nodeDescription); Condition versionCondition; if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) { - PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty(); + PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription) + .getRequiredVersionProperty(); versionCondition = rootNode.property(versionProperty.getName()) - .isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0))); - } else { + .isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0))); + } + else { versionCondition = Cypher.noCondition(); } - return match(rootNode) - .where(idDescription.asIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID))) - .and(versionCondition).unwind(rootNode.labels()).as("label").with(Cypher.name("label")) - .where(Cypher.name("label").in(parameter(Constants.NAME_OF_STATIC_LABELS_PARAM)).not()) - .returning(collect(Cypher.name("label")).as(Constants.NAME_OF_LABELS)).build(); + return match(rootNode).where(idDescription.asIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID))) + .and(versionCondition) + .unwind(rootNode.labels()) + .as("label") + .with(Cypher.name("label")) + .where(Cypher.name("label").in(parameter(Constants.NAME_OF_STATIC_LABELS_PARAM)).not()) + .returning(collect(Cypher.name("label")).as(Constants.NAME_OF_LABELS)) + .build(); } public Statement prepareDeleteOf(NodeDescription nodeDescription) { @@ -270,7 +355,7 @@ public enum CypherGenerator { public Statement prepareDeleteOf(NodeDescription nodeDescription, @Nullable Condition condition, boolean count) { Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels()) - .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); OngoingUpdate ongoingUpdate = match(rootNode).where(conditionOrNoCondition(condition)).detachDelete(rootNode); if (count) { return ongoingUpdate.returning(Cypher.count(rootNode)).build(); @@ -278,7 +363,8 @@ public enum CypherGenerator { return ongoingUpdate.build(); } - public Condition createCompositePropertyCondition(GraphPropertyDescription idProperty, SymbolicName containerName, Expression actualParameter) { + public Condition createCompositePropertyCondition(GraphPropertyDescription idProperty, SymbolicName containerName, + Expression actualParameter) { if (!idProperty.isComposite()) { return Cypher.property(containerName, idProperty.getPropertyName()).isEqualTo(actualParameter); @@ -300,7 +386,8 @@ public enum CypherGenerator { String primaryLabel = nodeDescription.getPrimaryLabel(); List additionalLabels = nodeDescription.getAdditionalLabels(); - Node rootNode = node(primaryLabel, additionalLabels).named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); + Node rootNode = node(primaryLabel, additionalLabels) + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); IdDescription idDescription = nodeDescription.getIdDescription(); Assert.notNull(idDescription, "Cannot save individual nodes without an id attribute"); Parameter idParameter = parameter(Constants.NAME_OF_ID); @@ -308,24 +395,30 @@ public enum CypherGenerator { Function vectorProcedureCall = (bs) -> { if (((Neo4jPersistentEntity) nodeDescription).hasVectorProperty()) { return bs.with(rootNode) - .call("db.create.setNodeVectorProperty") - .withArgs(rootNode.getRequiredSymbolicName(), parameter(Constants.NAME_OF_VECTOR_PROPERTY), parameter(Constants.NAME_OF_VECTOR_VALUE)) - .withoutResults() - .returning(rootNode).build(); + .call("db.create.setNodeVectorProperty") + .withArgs(rootNode.getRequiredSymbolicName(), parameter(Constants.NAME_OF_VECTOR_PROPERTY), + parameter(Constants.NAME_OF_VECTOR_VALUE)) + .withoutResults() + .returning(rootNode) + .build(); } return bs.returning(rootNode).build(); }; if (idDescription != null && !idDescription.isInternallyGeneratedId()) { - GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity) nodeDescription).getRequiredIdProperty(); + GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity) nodeDescription) + .getRequiredIdProperty(); if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) { - Property versionProperty = rootNode.property(((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty().getName()); + Property versionProperty = rootNode + .property(((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty().getName()); String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); - Statement createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) - .where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter)) + Statement createIfNew = vectorProcedureCall + .apply(updateDecorator.apply(optionalMatch(possibleExistingNode) + .where(createCompositePropertyCondition(idPropertyDescription, + possibleExistingNode.getRequiredSymbolicName(), idParameter)) .with(possibleExistingNode) .where(possibleExistingNode.isNull()) .create(rootNode.withProperties(versionProperty, literalOf(0))) @@ -333,34 +426,42 @@ public enum CypherGenerator { .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); Statement updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) - .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter)) - .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check - .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock - .with(rootNode) - .where(versionProperty.isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add( - literalOf(1)))) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), + idParameter)) + .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial + // check + .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire + // lock + .with(rootNode) + .where(versionProperty.isEqualTo( + coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add(literalOf(1)))) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); return Cypher.union(createIfNew, updateIfExists); - } else { + } + else { String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); - Statement createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) - .where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter)) - .with(possibleExistingNode) - .where(possibleExistingNode.isNull()) - .create(rootNode) - .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + Statement createIfNew = vectorProcedureCall + .apply(updateDecorator.apply(optionalMatch(possibleExistingNode) + .where(createCompositePropertyCondition(idPropertyDescription, + possibleExistingNode.getRequiredSymbolicName(), idParameter)) + .with(possibleExistingNode) + .where(possibleExistingNode.isNull()) + .create(rootNode) + .with(rootNode) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); Statement updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) - .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter)) - .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), + idParameter)) + .with(rootNode) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); return Cypher.union(createIfNew, updateIfExists); } - } else { + } + else { String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); @@ -368,32 +469,41 @@ public enum CypherGenerator { var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity, canUseElementId); if (neo4jPersistentEntity.hasVersionProperty()) { - Property versionProperty = rootNode.property(neo4jPersistentEntity.getRequiredVersionProperty().getName()); + Property versionProperty = rootNode + .property(neo4jPersistentEntity.getRequiredVersionProperty().getName()); var createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) + .where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) + .with(possibleExistingNode) + .where(possibleExistingNode.isNull()) + .create(rootNode.withProperties(versionProperty, literalOf(0))) + .with(rootNode) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + + var updateIfExists = vectorProcedureCall.apply(updateDecorator + .apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) + .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial + // check + .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire + // lock + .with(rootNode) + .where(versionProperty.isEqualTo( + coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add(literalOf(1)))) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + return Cypher.union(createIfNew, updateIfExists); + } + else { + var createStatement = vectorProcedureCall + .apply(updateDecorator.apply(optionalMatch(possibleExistingNode) .where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) .with(possibleExistingNode) .where(possibleExistingNode.isNull()) - .create(rootNode.withProperties(versionProperty, literalOf(0))) - .with(rootNode) + .create(rootNode) + .set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + var updateStatement = vectorProcedureCall.apply(updateDecorator + .apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - var updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) - .where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) - .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check - .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock - .with(rootNode) - .where(versionProperty.isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add( - literalOf(1)))) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - return Cypher.union(createIfNew, updateIfExists); - } else { - var createStatement = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode).where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) - .with(possibleExistingNode).where(possibleExistingNode.isNull()).create(rootNode) - .set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - var updateStatement = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - return Cypher.union(createStatement, updateStatement); } @@ -407,176 +517,135 @@ public enum CypherGenerator { "Only entities that use external IDs can be saved in a batch"); Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels()) - .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); IdDescription idDescription = nodeDescription.getIdDescription(); - @SuppressWarnings("ConstantConditions") // We now already that the node is using internal ids, and as such, an IdDescription must be present - String nameOfIdProperty = Optional.ofNullable(idDescription).flatMap(IdDescription::getOptionalGraphPropertyName) - .orElseThrow(() -> new MappingException("External id does not correspond to a graph property")); + @SuppressWarnings("ConstantConditions") // We now already that the node is using + // internal ids, and as such, an + // IdDescription must be present + String nameOfIdProperty = Optional.ofNullable(idDescription) + .flatMap(IdDescription::getOptionalGraphPropertyName) + .orElseThrow(() -> new MappingException("External id does not correspond to a graph property")); List expressions = new ArrayList<>(); if (nodeDescription instanceof Neo4jPersistentEntity entity && entity.isUsingDeprecatedInternalId()) { rootNode.internalId().as(Constants.NAME_OF_INTERNAL_ID); } - expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID)); + expressions.add(this.elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID)); expressions.add(rootNode.property(nameOfIdProperty).as(Constants.NAME_OF_ID)); String row = "entity"; - return Cypher.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)).as(row) - .merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID))) - .mutate(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM)) - .returning(expressions) - .build(); + return Cypher.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)) + .as(row) + .merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID))) + .mutate(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM)) + .returning(expressions) + .build(); } public Statement prepareSaveOfRelationship(Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationship, String dynamicRelationshipType, boolean canUseElementId) { - final Node startNode = neo4jPersistentEntity.isUsingInternalIds() - ? anyNode(START_NODE_NAME) + final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME) : node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) - .named(START_NODE_NAME); + .named(START_NODE_NAME); final Node endNode = anyNode(END_NODE_NAME); Parameter idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME); String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType(); - Relationship relationshipFragment = (relationship.isOutgoing() ? - startNode.relationshipTo(endNode, type) : - startNode.relationshipFrom(endNode, type)).named(RELATIONSHIP_NAME); + Relationship relationshipFragment = (relationship.isOutgoing() ? startNode.relationshipTo(endNode, type) + : startNode.relationshipFrom(endNode, type)) + .named(RELATIONSHIP_NAME); var startNodeIdFunction = getNodeIdFunction(neo4jPersistentEntity, canUseElementId); - return match(startNode) - .where(startNodeIdFunction.apply(startNode).isEqualTo(idParameter)) - .match(endNode) - .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME))) - .merge(relationshipFragment) - .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) - .build(); - } - - @SuppressWarnings("deprecation") - private static Function getNodeIdFunction(Neo4jPersistentEntity entity, boolean canUseElementId) { - - Function startNodeIdFunction; - var idProperty = entity.getRequiredIdProperty(); - if (entity.isUsingInternalIds()) { - if (entity.isUsingDeprecatedInternalId() || !canUseElementId) { - startNodeIdFunction = Node::internalId; - } else { - startNodeIdFunction = Cypher::elementId; - } - } else { - startNodeIdFunction = node -> node.property(idProperty.getPropertyName()); - } - return startNodeIdFunction; - } - - @SuppressWarnings("deprecation") - private static Function getEndNodeIdFunction(Neo4jPersistentEntity entity, boolean canUseElementId) { - - Function startNodeIdFunction; - if (entity == null) { - return Cypher::elementId; - } - if (!entity.isUsingDeprecatedInternalId() && canUseElementId) { - startNodeIdFunction = Cypher::elementId; - } else { - startNodeIdFunction = Node::internalId; - } - return startNodeIdFunction; - } - - static Expression relId(Relationship r) { - return FunctionInvocation.create(() -> "id", r.getRequiredSymbolicName()); - } - - private static Function getRelationshipIdFunction(RelationshipDescription relationshipDescription, boolean canUseElementId) { - - Function result = canUseElementId ? Cypher::elementId : CypherGenerator::relId; - if (relationshipDescription.hasRelationshipProperties()) { - Neo4jPersistentEntity entity = (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(); - if ((entity != null && entity.isUsingDeprecatedInternalId()) || !canUseElementId) { - result = CypherGenerator::relId; - } else { - result = Cypher::elementId; - } - } - return result; + return match(startNode).where(startNodeIdFunction.apply(startNode).isEqualTo(idParameter)) + .match(endNode) + .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId) + .apply(endNode) + .isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME))) + .merge(relationshipFragment) + .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) + .build(); } public Statement prepareSaveOfRelationships(Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationship, @Nullable String dynamicRelationshipType, boolean canUseElementId) { - final Node startNode = neo4jPersistentEntity.isUsingInternalIds() - ? anyNode(START_NODE_NAME) + final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME) : node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) - .named(START_NODE_NAME); + .named(START_NODE_NAME); final Node endNode = anyNode(END_NODE_NAME); String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType(); - Relationship relationshipFragment = (relationship.isOutgoing() ? - startNode.relationshipTo(endNode, type) : // CypherDSL is fine with a null type - startNode.relationshipFrom(endNode, type)).named(RELATIONSHIP_NAME); + Relationship relationshipFragment = (relationship.isOutgoing() ? startNode.relationshipTo(endNode, type) : // CypherDSL + // is + // fine + // with + // a + // null + // type + startNode.relationshipFrom(endNode, type)) + .named(RELATIONSHIP_NAME); String row = "relationship"; Property idProperty = Cypher.property(row, Constants.FROM_ID_PARAMETER_NAME); - return Cypher.unwind(parameter(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM)).as(row) - .with(row) - .match(startNode) - .where(getNodeIdFunction(neo4jPersistentEntity, canUseElementId).apply(startNode).isEqualTo(idProperty)) - .match(endNode) - .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME))) - .merge(relationshipFragment) - .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) - .build(); + return Cypher.unwind(parameter(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM)) + .as(row) + .with(row) + .match(startNode) + .where(getNodeIdFunction(neo4jPersistentEntity, canUseElementId).apply(startNode).isEqualTo(idProperty)) + .match(endNode) + .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId) + .apply(endNode) + .isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME))) + .merge(relationshipFragment) + .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) + .build(); } public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationship, - boolean isNew, - @Nullable String dynamicRelationshipType, - boolean canUseElementId, - boolean matchOnly) { + RelationshipDescription relationship, boolean isNew, @Nullable String dynamicRelationshipType, + boolean canUseElementId, boolean matchOnly) { Assert.isTrue(relationship.hasRelationshipProperties(), "Properties required to create a relationship with properties"); - Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()).named(START_NODE_NAME); + Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) + .named(START_NODE_NAME); Node endNode = anyNode(END_NODE_NAME); Parameter idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME); Parameter relationshipProperties = parameter(Constants.NAME_OF_PROPERTIES_PARAM); String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType(); - Relationship relationshipFragment = ( - relationship.isOutgoing() ? - startNode.relationshipTo(endNode, type) : - startNode.relationshipFrom(endNode, type)) - .named(RELATIONSHIP_NAME); + Relationship relationshipFragment = (relationship.isOutgoing() ? startNode.relationshipTo(endNode, type) + : startNode.relationshipFrom(endNode, type)) + .named(RELATIONSHIP_NAME); var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity, canUseElementId); var relationshipIdFunction = getRelationshipIdFunction(relationship, canUseElementId); StatementBuilder.OngoingReadingWithWhere startAndEndNodeMatch = match(startNode) - .where(nodeIdFunction.apply(startNode).isEqualTo(idParameter)) - .match(endNode) - .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME))); + .where(nodeIdFunction.apply(startNode).isEqualTo(idParameter)) + .match(endNode) + .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId) + .apply(endNode) + .isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME))); if (matchOnly) { return startAndEndNodeMatch.match(relationshipFragment) - .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) - .build(); - } - - StatementBuilder.ExposesSet createOrMatch = isNew - ? startAndEndNodeMatch.create(relationshipFragment) - : startAndEndNodeMatch.match(relationshipFragment) - .where(relationshipIdFunction.apply(relationshipFragment).isEqualTo(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM))); - return createOrMatch - .mutate(RELATIONSHIP_NAME, relationshipProperties) .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) .build(); + } + + StatementBuilder.ExposesSet createOrMatch = isNew ? startAndEndNodeMatch.create(relationshipFragment) + : startAndEndNodeMatch.match(relationshipFragment) + .where(relationshipIdFunction.apply(relationshipFragment) + .isEqualTo(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM))); + return createOrMatch.mutate(RELATIONSHIP_NAME, relationshipProperties) + .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) + .build(); } public Statement prepareUpdateOfRelationshipsWithProperties(Neo4jPersistentEntity neo4jPersistentEntity, @@ -585,63 +654,67 @@ public enum CypherGenerator { Assert.isTrue(relationship.hasRelationshipProperties(), "Properties required to create a relationship with properties"); - Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()).named(START_NODE_NAME); + Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) + .named(START_NODE_NAME); Node endNode = anyNode(END_NODE_NAME); String type = relationship.getType(); - Relationship relationshipFragment = ( - relationship.isOutgoing() ? - startNode.relationshipTo(endNode, type) : - startNode.relationshipFrom(endNode, type)) - .named(RELATIONSHIP_NAME); + Relationship relationshipFragment = (relationship.isOutgoing() ? startNode.relationshipTo(endNode, type) + : startNode.relationshipFrom(endNode, type)) + .named(RELATIONSHIP_NAME); String row = "row"; Property relationshipProperties = Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM); Property idProperty = Cypher.property(row, Constants.FROM_ID_PARAMETER_NAME); - StatementBuilder.OrderableOngoingReadingAndWithWithoutWhere cypherUnwind = Cypher.unwind(parameter(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM)) - .as(row) - .with(row); + StatementBuilder.OrderableOngoingReadingAndWithWithoutWhere cypherUnwind = Cypher + .unwind(parameter(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM)) + .as(row) + .with(row); var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity, canUseElementId); var relationshipIdFunction = getRelationshipIdFunction(relationship, canUseElementId); - // we only need start and end node querying if we have to create a new relationship... + // we only need start and end node querying if we have to create a new + // relationship... if (isNew) { - return cypherUnwind - .match(startNode) - .where(nodeIdFunction.apply(startNode).isEqualTo(idProperty)) - .match(endNode) - .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME))) - .create(relationshipFragment) - .mutate(RELATIONSHIP_NAME, relationshipProperties) - .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) - .build(); + return cypherUnwind.match(startNode) + .where(nodeIdFunction.apply(startNode).isEqualTo(idProperty)) + .match(endNode) + .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId) + .apply(endNode) + .isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME))) + .create(relationshipFragment) + .mutate(RELATIONSHIP_NAME, relationshipProperties) + .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) + .build(); } // ... otherwise we can just fetch the existing relationship by known id return cypherUnwind.match(relationshipFragment) - .where(relationshipIdFunction.apply(relationshipFragment).isEqualTo(Cypher.property(row, Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM))) - .mutate(RELATIONSHIP_NAME, relationshipProperties).build(); + .where(relationshipIdFunction.apply(relationshipFragment) + .isEqualTo(Cypher.property(row, Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM))) + .mutate(RELATIONSHIP_NAME, relationshipProperties) + .build(); } - private List getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) { + private List getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, + Relationship relationshipFragment) { List result = new ArrayList<>(); - if (relationship.hasRelationshipProperties() && relationship.getRelationshipPropertiesEntity() instanceof Neo4jPersistentEntity entity && entity.isUsingDeprecatedInternalId()) { + if (relationship.hasRelationshipProperties() + && relationship.getRelationshipPropertiesEntity() instanceof Neo4jPersistentEntity entity + && entity.isUsingDeprecatedInternalId()) { result.add(relId(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID)); } - result.add(elementIdOrIdFunction.apply(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)); + result.add(this.elementIdOrIdFunction.apply(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)); return result; } - public Statement prepareDeleteOf( - Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationshipDescription, - boolean canUseElementId - ) { + public Statement prepareDeleteOf(Neo4jPersistentEntity neo4jPersistentEntity, + RelationshipDescription relationshipDescription, boolean canUseElementId) { final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME) : node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()) - .named(START_NODE_NAME); + .named(START_NODE_NAME); NodeDescription target = relationshipDescription.getTarget(); Node endNode = node(target.getPrimaryLabel(), target.getAdditionalLabels()); @@ -657,10 +730,12 @@ public enum CypherGenerator { Parameter idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME); return match(relationship) - .where(getNodeIdFunction(neo4jPersistentEntity, canUseElementId).apply(startNode).isEqualTo(idParameter)) - .and(getRelationshipIdFunction(relationshipDescription, canUseElementId).apply(relationship).in(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM)).not()) - .delete(relationship.getRequiredSymbolicName()) - .build(); + .where(getNodeIdFunction(neo4jPersistentEntity, canUseElementId).apply(startNode).isEqualTo(idParameter)) + .and(getRelationshipIdFunction(relationshipDescription, canUseElementId).apply(relationship) + .in(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM)) + .not()) + .delete(relationship.getRequiredSymbolicName()) + .build(); } public Collection createReturnStatementForExists(Neo4jPersistentEntity nodeDescription) { @@ -673,65 +748,70 @@ public enum CypherGenerator { } /** - * Creates an order by fragment, assuming the node to match is named `n` - * - * @param sort The {@link Sort sort} that should be turned into a valid Cypher {@code ORDER}-clause - * @return An optional order clause. Will be {@literal null} on sorts that are {@literal null} or unsorted. + * Creates an order by fragment, assuming the node to match is named `n`. + * @param sort the {@link Sort sort} that should be turned into a valid Cypher + * {@code ORDER}-clause + * @return an optional order clause. Will be {@literal null} on sorts that are + * {@literal null} or unsorted */ - @Nullable - public String createOrderByFragment(@Nullable Sort sort) { + @Nullable public String createOrderByFragment(@Nullable Sort sort) { if (sort == null || sort.isUnsorted()) { return null; } - Statement statement = match(anyNode()).returning("n") - .orderBy(sort.stream().map(order -> { - String property = order.getProperty().trim(); - Expression expression; - if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) { - expression = Cypher.raw(property); - } else if (property.contains(".")) { - 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(property.substring(0, firstDot), tail); - } else { - try { - Assert.isTrue(SourceVersion.isIdentifier(property), "Name must be a valid identifier."); - expression = Cypher.name(property); - } catch (IllegalArgumentException ex) { - var msg = Optional.ofNullable(ex.getMessage()).orElse(""); - if (msg.endsWith(".")) { - throw new IllegalArgumentException(msg.substring(0, msg.length() - 1)); - } - throw ex; - } + Statement statement = match(anyNode()).returning("n").orderBy(sort.stream().map(order -> { + String property = order.getProperty().trim(); + Expression expression; + if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) { + expression = Cypher.raw(property); + } + else if (property.contains(".")) { + int firstDot = property.indexOf('.'); + String tail = property.substring(firstDot + 1); + if (tail.isEmpty() || property.lastIndexOf(".") != firstDot) { + if (tail.trim().matches("`.+`")) { + tail = tail.replaceFirst("`(.+)`", "$1"); } - if (order.isIgnoreCase()) { - expression = Cypher.toLower(expression); + else { + throw new IllegalArgumentException(String.format( + "Cannot handle order property `%s`, it must be a simple property or one-hop path", + property)); } - return order.isAscending() ? expression.ascending() : expression.descending(); - }).toArray(SortItem[]::new)) - .build(); + } + expression = Cypher.property(property.substring(0, firstDot), tail); + } + else { + try { + Assert.isTrue(SourceVersion.isIdentifier(property), "Name must be a valid identifier."); + expression = Cypher.name(property); + } + catch (IllegalArgumentException ex) { + var msg = Optional.ofNullable(ex.getMessage()).orElse(""); + if (msg.endsWith(".")) { + throw new IllegalArgumentException(msg.substring(0, msg.length() - 1)); + } + throw ex; + } + } + if (order.isIgnoreCase()) { + expression = Cypher.toLower(expression); + } + return order.isAscending() ? expression.ascending() : expression.descending(); + }).toArray(SortItem[]::new)).build(); String renderedStatement = Renderer.getRenderer(Configuration.defaultConfig()).render(statement); return renderedStatement.substring(renderedStatement.indexOf("ORDER BY")).trim(); } /** - * @param nodeDescription Description of the root node - * @param includeField A predicate derived from the set of included properties. This is only relevant in various forms - * of projections which allow to exclude one or more fields. - * @param additionalExpressions any additional expressions to add to the return statement - * @return An expression to be returned by a Cypher statement + * Creates a return statement for an ongoing match. + * @param nodeDescription description of the root node + * @param includeField a predicate derived from the set of included properties. This + * is only relevant in various forms of projections which allow to exclude one or more + * fields + * @param additionalExpressions any additional expressions to add to the return + * statement + * @return an expression to be returned by a Cypher statement */ public Collection createReturnStatementForMatch(Neo4jPersistentEntity nodeDescription, Predicate includeField, Expression... additionalExpressions) { @@ -739,14 +819,13 @@ public enum CypherGenerator { if (nodeDescription.containsPossibleCircles(includeField)) { return createGenericReturnStatement(additionalExpressions); - } else { + } + else { List returnContent = new ArrayList<>(); returnContent.add(projectPropertiesAndRelationships( - PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()), - nodeDescription, - Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), - includeField, - processedRelationships)); + PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()), + nodeDescription, Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), includeField, + processedRelationships)); Collections.addAll(returnContent, additionalExpressions); return returnContent; } @@ -761,33 +840,46 @@ public enum CypherGenerator { return returnExpressions; } - - public StatementBuilder.OngoingReading prepareFindOf(NodeDescription nodeDescription, List initialMatchOn, @Nullable Condition condition) { + public StatementBuilder.OngoingReading prepareFindOf(NodeDescription nodeDescription, + List initialMatchOn, @Nullable Condition condition) { var rootNode = createRootNode(nodeDescription); return prepareMatchOfRootNode(rootNode, initialMatchOn).where(conditionOrNoCondition(condition)); } - private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity nodeDescription, SymbolicName nodeName, - Predicate includedProperties, @Nullable List processedRelationships) { + private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, + Neo4jPersistentEntity nodeDescription, SymbolicName nodeName, + Predicate includedProperties, + @Nullable List processedRelationships) { - Collection relationships = ((DefaultNeo4jPersistentEntity) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath); + Collection relationships = ((DefaultNeo4jPersistentEntity) nodeDescription) + .getRelationshipsInHierarchy(includedProperties, parentPath); relationships.removeIf(r -> !includedProperties.test(parentPath.append(r.getFieldName()))); - List propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, includedProperties); + List propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, + includedProperties); List contentOfProjection = new ArrayList<>(propertiesProjection); - contentOfProjection.addAll(generateListsFor(parentPath, nodeDescription, relationships, nodeName, includedProperties, processedRelationships)); + contentOfProjection.addAll(generateListsFor(parentPath, nodeDescription, relationships, nodeName, + includedProperties, processedRelationships)); return Cypher.anyNode(nodeName).project(contentOfProjection); } /** - * Creates a list of objects that represents a very basic of {@code MapEntry} with the exception that - * this list can also contain two "keys" in a row. The {@link MapProjection} will take care to handle them as - * self-reflecting fields. Example with self-reflection and explicit value: {@code n {.id, name: n.name}}. + * Creates a list of objects that represents a very basic of + * {@code MapEntry} with the exception that this list can also contain + * two "keys" in a row. The {@link MapProjection} will take care to handle them as + * self-reflecting fields. Example with self-reflection and explicit value: {@code n + * {.id, name: n.name}}. + * @param parentPath parent path + * @param nodeDescription the description to work on + * @param nodeName the name of the node to project from + * @param includeField a predicate to decide on including fields or not + * @return a list of projected properties */ @SuppressWarnings("deprecation") - private List projectNodeProperties(PropertyFilter.RelaxedPropertyPath parentPath, NodeDescription nodeDescription, SymbolicName nodeName, - Predicate includeField) { + private List projectNodeProperties(PropertyFilter.RelaxedPropertyPath parentPath, + NodeDescription nodeDescription, SymbolicName nodeName, + Predicate includeField) { List nodePropertiesProjection = new ArrayList<>(); Node node = anyNode(nodeName); @@ -808,7 +900,8 @@ public enum CypherGenerator { } // ignore internally generated id fields - if (graphProperty.isIdProperty() && (nodeDescription.getIdDescription() != null && nodeDescription.getIdDescription().isInternallyGeneratedId())) { + if (graphProperty.isIdProperty() && (nodeDescription.getIdDescription() != null + && nodeDescription.getIdDescription().isInternallyGeneratedId())) { continue; } nodePropertiesProjection.add(graphProperty.getPropertyName()); @@ -826,15 +919,14 @@ public enum CypherGenerator { nodePropertiesProjection.add(node.internalId()); } nodePropertiesProjection.add(Constants.NAME_OF_ELEMENT_ID); - nodePropertiesProjection.add(elementIdOrIdFunction.apply(node)); + nodePropertiesProjection.add(this.elementIdOrIdFunction.apply(node)); return nodePropertiesProjection; } - /** - * @see CypherGenerator#projectNodeProperties - */ - private List generateListsFor(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity nodeDescription, Collection relationships, SymbolicName nodeName, - Predicate includedProperties, @Nullable List processedRelationships) { + private List generateListsFor(PropertyFilter.RelaxedPropertyPath parentPath, + Neo4jPersistentEntity nodeDescription, Collection relationships, + SymbolicName nodeName, Predicate includedProperties, + @Nullable List processedRelationships) { List mapProjectionLists = new ArrayList<>(); List processed = Objects.requireNonNullElseGet(processedRelationships, ArrayList::new); @@ -843,28 +935,33 @@ public enum CypherGenerator { String fieldName = relationshipDescription.getFieldName(); - // if we already processed the other way before, do not try to jump in the infinite loop + // if we already processed the other way before, do not try to jump in the + // infinite loop // unless it is a root node relationship if (relationshipDescription.hasRelationshipObverse() && processed.contains(relationshipDescription.getRelationshipObverse())) { continue; } - generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processed, fieldName, mapProjectionLists, includedProperties); + generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processed, fieldName, + mapProjectionLists, includedProperties); } return mapProjectionLists; } - private void generateListFor(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity nodeDescription, RelationshipDescription relationshipDescription, SymbolicName nodeName, - List processedRelationships, String fieldName, List mapProjectionLists, Predicate includedProperties) { + private void generateListFor(PropertyFilter.RelaxedPropertyPath parentPath, + Neo4jPersistentEntity nodeDescription, RelationshipDescription relationshipDescription, + SymbolicName nodeName, List processedRelationships, String fieldName, + List mapProjectionLists, Predicate includedProperties) { String relationshipType = relationshipDescription.getType(); String relationshipTargetName = relationshipDescription.generateRelatedNodesCollectionName(nodeDescription); String sourcePrimaryLabel = relationshipDescription.getSource().getMostAbstractParentLabel(nodeDescription); String targetPrimaryLabel = relationshipDescription.getTarget().getPrimaryLabel(); List targetAdditionalLabels = relationshipDescription.getTarget().getAdditionalLabels(); - String relationshipSymbolicName = sourcePrimaryLabel + RelationshipDescription.NAME_OF_RELATIONSHIP + targetPrimaryLabel; + String relationshipSymbolicName = sourcePrimaryLabel + RelationshipDescription.NAME_OF_RELATIONSHIP + + targetPrimaryLabel; Node startNode = anyNode(nodeName); SymbolicName relationshipFieldName = nodeName.concat("_" + fieldName); @@ -875,21 +972,20 @@ public enum CypherGenerator { PropertyFilter.RelaxedPropertyPath newParentPath; newParentPath = parentPath.append(relationshipDescription.getFieldName()); if (relationshipDescription.hasRelationshipProperties()) { - var persistentProperty = ((Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class); + var persistentProperty = ((Neo4jPersistentEntity) relationshipDescription + .getRequiredRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class); if (persistentProperty != null) { - newParentPath = newParentPath - .append(persistentProperty.getFieldName()); + newParentPath = newParentPath.append(persistentProperty.getFieldName()); } } if (relationshipDescription.isDynamic()) { - Relationship relationship = relationshipDescription.isOutgoing() - ? startNode.relationshipTo(endNode) + Relationship relationship = relationshipDescription.isOutgoing() ? startNode.relationshipTo(endNode) : startNode.relationshipFrom(endNode); relationship = relationship.named(relationshipTargetName); - MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName, - includedProperties, new ArrayList<>(processedRelationships)); + MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, + relationshipFieldName, includedProperties, new ArrayList<>(processedRelationships)); if (relationshipDescription.hasRelationshipProperties()) { relationship = relationship.named(relationshipSymbolicName); @@ -898,23 +994,25 @@ public enum CypherGenerator { addMapProjection(relationshipTargetName, listBasedOn(relationship).returning(mapProjection - .and(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE, Cypher.type(relationship))), + .and(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE, Cypher.type(relationship))), mapProjectionLists); - } else { + } + else { Relationship relationship = relationshipDescription.isOutgoing() ? startNode.relationshipTo(endNode, relationshipType) : startNode.relationshipFrom(endNode, relationshipType); - MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName, - includedProperties, new ArrayList<>(processedRelationships)); + MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, + relationshipFieldName, includedProperties, new ArrayList<>(processedRelationships)); if (relationshipDescription.hasRelationshipProperties()) { relationship = relationship.named(relationshipSymbolicName); mapProjection = mapProjection.and(relationship); } - addMapProjection(relationshipTargetName, listBasedOn(relationship).returning(mapProjection), mapProjectionLists); + addMapProjection(relationshipTargetName, listBasedOn(relationship).returning(mapProjection), + mapProjectionLists); } } @@ -923,7 +1021,4 @@ public enum CypherGenerator { projectionList.add(projection); } - private static Condition conditionOrNoCondition(@Nullable Condition condition) { - return condition == null ? Cypher.noCondition() : condition; - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java index 2902882cd..c4fb4ee50 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java @@ -24,6 +24,7 @@ import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; @@ -36,14 +37,18 @@ import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConver import org.springframework.data.util.TypeInformation; /** + * Default implementation for all {@link Neo4jConversionService Neo4j specific conversion + * services}. + * * @author Michael J. Simons - * @soundtrack Die Ärzte - Die Nacht der Dämonen * @since 6.0 */ final class DefaultNeo4jConversionService implements Neo4jConversionService { private final ConversionService conversionService; + private final Predicate> hasCustomWriteTargetPredicate; + private final SimpleTypeHolder simpleTypes; DefaultNeo4jConversionService(Neo4jConversions neo4jConversions) { @@ -56,36 +61,39 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { this.simpleTypes = neo4jConversions.getSimpleTypeHolder(); } + private static boolean isCollection(TypeInformation type) { + return Collection.class.isAssignableFrom(type.getType()); + } + @Override - @Nullable - public T convert(Object source, Class targetType) { - return conversionService.convert(source, targetType); + @Nullable public T convert(Object source, Class targetType) { + return this.conversionService.convert(source, targetType); } @Override public boolean hasCustomWriteTarget(Class sourceType) { - return hasCustomWriteTargetPredicate.test(sourceType); + return this.hasCustomWriteTargetPredicate.test(sourceType); } @Override - @Nullable - public Object readValue(@Nullable Value source, TypeInformation targetType, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { + @Nullable public Object readValue(@Nullable Value source, TypeInformation targetType, + @Nullable Neo4jPersistentPropertyConverter conversionOverride) { BiFunction, Object> conversion; boolean applyConversionToCompleteCollection = false; if (conversionOverride == null) { - conversion = conversionService::convert; - } else { + conversion = this.conversionService::convert; + } + else { applyConversionToCompleteCollection = conversionOverride instanceof NullSafeNeo4jPersistentPropertyConverter - && ((NullSafeNeo4jPersistentPropertyConverter) conversionOverride).isForCollection(); + && ((NullSafeNeo4jPersistentPropertyConverter) conversionOverride).isForCollection(); conversion = (v, t) -> conversionOverride.read(v); } return readValueImpl(source, targetType, conversion, applyConversionToCompleteCollection); } - @Nullable - private Object readValueImpl(@Nullable Value value, TypeInformation type, + @Nullable private Object readValueImpl(@Nullable Value value, TypeInformation type, BiFunction, Object> conversion, boolean applyConversionToCompleteCollection) { boolean valueIsLiteralNullOrNullValue = value == null || value == Values.NULL; @@ -96,16 +104,17 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { if (!valueIsLiteralNullOrNullValue && isCollection(type) && !applyConversionToCompleteCollection) { // value can't be null at this point in time @SuppressWarnings("NullAway") - Collection target = CollectionFactory - .createCollection(rawType, Objects.requireNonNull(type.getComponentType()).getType(), value.size()); + Collection target = CollectionFactory.createCollection(rawType, + Objects.requireNonNull(type.getComponentType()).getType(), value.size()); value.values() - .forEach(element -> target.add(conversion.apply(element, type.getComponentType().getType()))); + .forEach(element -> target.add(conversion.apply(element, type.getComponentType().getType()))); return target; } return valueIsLiteralNullOrNullValue ? null : conversion.apply(value, rawType); - } catch (Exception e) { + } + catch (Exception ex) { String msg = String.format("Could not convert %s into %s", value, type); - throw new TypeMismatchDataAccessException(msg, e); + throw new TypeMismatchDataAccessException(msg, ex); } } @@ -116,26 +125,29 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { Function conversion; boolean applyConversionToCompleteCollection = false; if (writingConverter == null) { - conversion = v -> conversionService.convert(v, Value.class); - } else { + conversion = v -> this.conversionService.convert(v, Value.class); + } + else { @SuppressWarnings("unchecked") Neo4jPersistentPropertyConverter hlp = (Neo4jPersistentPropertyConverter) writingConverter; applyConversionToCompleteCollection = writingConverter instanceof NullSafeNeo4jPersistentPropertyConverter - && ((NullSafeNeo4jPersistentPropertyConverter) writingConverter).isForCollection(); + && ((NullSafeNeo4jPersistentPropertyConverter) writingConverter).isForCollection(); conversion = hlp::write; } return writeValueImpl(value, sourceType, conversion, applyConversionToCompleteCollection); } - private Value writeValueImpl(@Nullable Object value, TypeInformation type, - Function conversion, boolean applyConversionToCompleteCollection) { + private Value writeValueImpl(@Nullable Object value, TypeInformation type, Function conversion, + boolean applyConversionToCompleteCollection) { if (value == null) { try { - // Some conversion services may treat null special, so we pass it anyway and ask for forgiveness + // Some conversion services may treat null special, so we pass it anyway + // and ask for forgiveness return conversion.apply(null); - } catch (NullPointerException e) { + } + catch (NullPointerException ex) { return Values.NULL; } } @@ -149,12 +161,9 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService { return conversion.apply(value); } - private static boolean isCollection(TypeInformation type) { - return Collection.class.isAssignableFrom(type.getType()); - } - @Override public boolean isSimpleType(Class type) { - return simpleTypes.isSimpleType(type); + return this.simpleTypes.isSimpleType(type); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index 623a3485e..d024a288f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -46,6 +46,7 @@ import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.Type; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.CollectionFactory; import org.springframework.core.KotlinDetector; import org.springframework.data.mapping.AssociationHandler; @@ -63,16 +64,19 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** + * Default implementation for the {@link Neo4jEntityConverter}. + * * @author Michael J. Simons * @author Gerrit Meier * @author Philipp Tölle - * @soundtrack The Kleptones - A Night At The Hip-Hopera * @since 6.0 */ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private final EntityInstantiators entityInstantiators; + private final NodeDescriptionStore nodeDescriptionStore; + private final Neo4jConversionService conversionService; private final EventSupport eventSupport; @@ -80,9 +84,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private final KnownObjects knownObjects = new KnownObjects(); private final Type nodeType; + private final Type relationshipType; + private final Type mapType; + private final Type listType; + private final Type pathType; private final Map> labelNodeCache = new HashMap<>(); @@ -107,192 +115,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { this.pathType = typeSystem.PATH(); } - @Override - public R read(Class targetType, MapAccessor mapAccessor) { - - knownObjects.nextRecord(); - labelNodeCache.clear(); - - @SuppressWarnings("unchecked") // ¯\_(ツ)_/¯ - Neo4jPersistentEntity rootNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType), () -> "Can't read an entity of type %s without description".formatted(targetType)); - MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription, true); - if (queryRoot == null) { - throw new IllegalStateException("No query root"); - } - - try { - return map(queryRoot, queryRoot, rootNodeDescription); - } catch (Exception e) { - throw new MappingException("Error mapping " + mapAccessor, e); - } - } - - @Nullable - private MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity rootNodeDescription, boolean firstTry) { - - if (rootNodeDescription == null) { - return null; - } - - List primaryLabels = new ArrayList<>(); - primaryLabels.add(rootNodeDescription.getPrimaryLabel()); - rootNodeDescription.getChildNodeDescriptionsInHierarchy().forEach(nodeDescription -> primaryLabels.add(nodeDescription.getPrimaryLabel())); - - // Massage the initial mapAccessor into something we can deal with - Iterable recordValues = mapAccessor instanceof Value && ((Value) mapAccessor).hasType(nodeType) ? - Collections.singletonList((Value) mapAccessor) : mapAccessor.values(); - - List matchingNodes = new ArrayList<>(); // The node that eventually becomes the query root. The list should only contain one node. - List seenMatchingNodes = new ArrayList<>(); // A list of candidates: All things that are nodes and have a matching label - - for (Value value : recordValues) { - if (value.hasType(nodeType)) { // It is a node - Node node = value.asNode(); - if (primaryLabels.stream().anyMatch(node::hasLabel)) { // it has a matching label - // We haven't seen this node yet, so we take it - if (knownObjects.getObject("N" + IdentitySupport.getElementId(node)) == null) { - matchingNodes.add(node); - } else { - seenMatchingNodes.add(node); - } - } - } - } - - // Prefer the candidates over candidates previously seen - List finalCandidates = matchingNodes.isEmpty() ? seenMatchingNodes : matchingNodes; - - if (finalCandidates.size() > 1) { - throw new MappingException("More than one matching node in the record"); - } else if (!finalCandidates.isEmpty()) { - if (mapAccessor.size() > 1) { - return mergeRootNodeWithRecord(finalCandidates.get(0), mapAccessor); - } else { - return finalCandidates.get(0); - } - } else { - int cnt = 0; - Value firstValue = Values.NULL; - for (Value value : recordValues) { - if (cnt == 0) { - firstValue = value; - } - if (value.hasType(mapType) && !(value.hasType(nodeType) || value.hasType(relationshipType))) { - return value; - } - ++cnt; - } - - // Cater for results that have one single, null column. This is the case for MATCH (x) OPTIONAL MATCH (something) RETURN something - if (cnt == 1 && firstValue.isNull()) { - return null; - } - } - - // The aggregating mapping function synthesizes a bunch of things and we must not interfere with those - boolean isSynthesized = isSynthesized(mapAccessor); - if (!isSynthesized) { - // Check if the original record has been a map. Would have been probably sane to do this right from the start, - // but this would change original SDN 6.0 behaviour to much - if (mapAccessor instanceof Value && ((Value) mapAccessor).hasType(mapType)) { - return mapAccessor; - } - - // This is also due the aggregating mapping function: It will check on a NoRootNodeMappingException - // whether there's a nested, aggregatable path - if (firstTry && !canBeAggregated(mapAccessor)) { - Value value = Values.value(Collections.singletonMap("_", mapAccessor.asMap(Function.identity()))); - return determineQueryRoot(value, rootNodeDescription, false); - } - } - - throw new NoRootNodeMappingException(mapAccessor, rootNodeDescription); - } - - private boolean canBeAggregated(MapAccessor mapAccessor) { - - if (mapAccessor instanceof Record r) { - return r.values().stream().anyMatch(pathType::isTypeOf); - } - return false; - } - - private boolean isSynthesized(MapAccessor mapAccessor) { - return mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) && - mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATIONS) && - mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); - } - - private Collection createDynamicLabelsProperty(TypeInformation type, Collection dynamicLabels) { - - Collection target = CollectionFactory.createCollection(type.getType(), String.class, dynamicLabels.size()); - target.addAll(dynamicLabels); - return target; - } - - @Override - public void write(Object source, Map parameters) { - - Neo4jPersistentEntity nodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore - .getNodeDescription(source.getClass()); - if (nodeDescription == null) { - return; - } - - Map properties = new HashMap<>(); - - if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) { - // add type info when write to the database - properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel()); - } - - PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); - PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> { - - // Skip the internal properties, we don't want them to end up stored as properties - if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() || p.isReadOnly() || p.isVectorProperty()) { - return; - } - - final Value value = conversionService.writeValue(propertyAccessor.getProperty(p), p.getTypeInformation(), p.getOptionalConverter()); - if (p.isComposite()) { - properties.put(p.getPropertyName(), new MapValueWrapper(value)); - } else { - properties.put(p.getPropertyName(), value); - } - }); - - parameters.put(Constants.NAME_OF_PROPERTIES_PARAM, properties); - - // in case of relationship properties ignore internal id property - if (nodeDescription.hasIdProperty()) { - Neo4jPersistentProperty idProperty = nodeDescription.getRequiredIdProperty(); - parameters.put(Constants.NAME_OF_ID, - conversionService.writeValue(propertyAccessor.getProperty(idProperty), idProperty.getTypeInformation(), idProperty.getOptionalConverter())); - } - // in case of relationship properties ignore internal id property - if (nodeDescription.hasVersionProperty()) { - Long versionProperty = (Long) propertyAccessor.getProperty(nodeDescription.getRequiredVersionProperty()); - - // we incremented this upfront the persist operation so the matching version would be one "before" - parameters.put(Constants.NAME_OF_VERSION_PARAM, versionProperty); - } - - // special handling for vector property to provide the needed procedure information - if (nodeDescription.hasVectorProperty()) { - Neo4jPersistentProperty vectorProperty = nodeDescription.getRequiredVectorProperty(); - parameters.put(Constants.NAME_OF_VECTOR_PROPERTY, vectorProperty.getPropertyName()); - parameters.put(Constants.NAME_OF_VECTOR_VALUE, conversionService.writeValue(propertyAccessor.getProperty(vectorProperty), vectorProperty.getTypeInformation(), vectorProperty.getOptionalConverter())); - } - } - /** - * Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too. - * Merge happens only when the record contains additional values. - * - * @param node Node whose attributes are about to be merged - * @param record Record that should be merged - * @return A map accessor combining a {@link Node} and an arbitrary record + * Merges the root node of a query and the remaining record into one map, adding the + * internal ID of the node, too. Merge happens only when the record contains + * additional values. + * @param node node whose attributes are about to be merged + * @param record record that should be merged + * @return a map accessor combining a {@link Node} and an arbitrary record */ @SuppressWarnings("deprecation") private static MapAccessor mergeRootNodeWithRecord(Node node, MapAccessor record) { @@ -307,12 +136,271 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return Values.value(mergedAttributes); } + private static Object getValueOrDefault(boolean ownerIsKotlinType, Class rawType, Object value) { + + return (value == null && !ownerIsKotlinType && rawType.isPrimitive()) + ? ReflectionUtils.getPrimitiveDefault(rawType) : value; + } + + @SuppressWarnings("deprecation") + private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) { + if (property.isInternalIdProperty()) { + if (Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(property.getType())) { + return Values.value(IdentitySupport.getInternalId(propertyContainer)); + } + return Values.value(IdentitySupport.getElementId(propertyContainer)); + } + else if (property.isComposite()) { + String prefix = property.computePrefixWithDelimiter(); + + if (propertyContainer.containsKey(Constants.NAME_OF_ALL_PROPERTIES)) { + return extractCompositePropertyValues(propertyContainer.get(Constants.NAME_OF_ALL_PROPERTIES), prefix); + } + else { + return extractCompositePropertyValues(propertyContainer, prefix); + } + } + else { + String graphPropertyName = property.getPropertyName(); + if (propertyContainer.containsKey(graphPropertyName)) { + return propertyContainer.get(graphPropertyName); + } + else if (propertyContainer.containsKey(Constants.NAME_OF_ALL_PROPERTIES)) { + return propertyContainer.get(Constants.NAME_OF_ALL_PROPERTIES).get(graphPropertyName); + } + else { + return Values.NULL; + } + } + } + + private static Value extractCompositePropertyValues(MapAccessor propertyContainer, String prefix) { + Map hlp = new HashMap<>(propertyContainer.size()); + propertyContainer.keys().forEach(k -> { + if (k.startsWith(prefix)) { + hlp.put(k, propertyContainer.get(k)); + } + }); + return Values.value(hlp); + } + + @Override + public R read(Class targetType, MapAccessor mapAccessor) { + + this.knownObjects.nextRecord(); + this.labelNodeCache.clear(); + + @SuppressWarnings("unchecked") // ¯\_(ツ)_/¯ + Neo4jPersistentEntity rootNodeDescription = Objects.requireNonNull( + (Neo4jPersistentEntity) this.nodeDescriptionStore.getNodeDescription(targetType), + () -> "Can't read an entity of type %s without description".formatted(targetType)); + MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription, true); + if (queryRoot == null) { + throw new IllegalStateException("No query root"); + } + + try { + return map(queryRoot, queryRoot, rootNodeDescription); + } + catch (Exception ex) { + throw new MappingException("Error mapping " + mapAccessor, ex); + } + } + + @Nullable private MapAccessor determineQueryRoot(MapAccessor mapAccessor, + @Nullable Neo4jPersistentEntity rootNodeDescription, boolean firstTry) { + + if (rootNodeDescription == null) { + return null; + } + + List primaryLabels = new ArrayList<>(); + primaryLabels.add(rootNodeDescription.getPrimaryLabel()); + rootNodeDescription.getChildNodeDescriptionsInHierarchy() + .forEach(nodeDescription -> primaryLabels.add(nodeDescription.getPrimaryLabel())); + + // Massage the initial mapAccessor into something we can deal with + Iterable recordValues = (mapAccessor instanceof Value && ((Value) mapAccessor).hasType(this.nodeType)) + ? Collections.singletonList((Value) mapAccessor) : mapAccessor.values(); + + List matchingNodes = new ArrayList<>(); // The node that eventually becomes + // the query root. The list should + // only contain one node. + List seenMatchingNodes = new ArrayList<>(); // A list of candidates: All + // things that are nodes and + // have a matching label + + for (Value value : recordValues) { + if (value.hasType(this.nodeType)) { // It is a node + Node node = value.asNode(); + if (primaryLabels.stream().anyMatch(node::hasLabel)) { // it has a + // matching label + // We haven't seen this node yet, so we take it + if (this.knownObjects.getObject("N" + IdentitySupport.getElementId(node)) == null) { + matchingNodes.add(node); + } + else { + seenMatchingNodes.add(node); + } + } + } + } + + // Prefer the candidates over candidates previously seen + List finalCandidates = matchingNodes.isEmpty() ? seenMatchingNodes : matchingNodes; + + if (finalCandidates.size() > 1) { + throw new MappingException("More than one matching node in the record"); + } + else if (!finalCandidates.isEmpty()) { + if (mapAccessor.size() > 1) { + return mergeRootNodeWithRecord(finalCandidates.get(0), mapAccessor); + } + else { + return finalCandidates.get(0); + } + } + else { + int cnt = 0; + Value firstValue = Values.NULL; + for (Value value : recordValues) { + if (cnt == 0) { + firstValue = value; + } + if (value.hasType(this.mapType) + && !(value.hasType(this.nodeType) || value.hasType(this.relationshipType))) { + return value; + } + ++cnt; + } + + // Cater for results that have one single, null column. This is the case for + // MATCH (x) OPTIONAL MATCH (something) RETURN something + if (cnt == 1 && firstValue.isNull()) { + return null; + } + } + + // The aggregating mapping function synthesizes a bunch of things and we must not + // interfere with those + boolean isSynthesized = isSynthesized(mapAccessor); + if (!isSynthesized) { + // Check if the original record has been a map. Would have been probably sane + // to do this right from the start, + // but this would change original SDN 6.0 behaviour to much + if (mapAccessor instanceof Value && ((Value) mapAccessor).hasType(this.mapType)) { + return mapAccessor; + } + + // This is also due the aggregating mapping function: It will check on a + // NoRootNodeMappingException + // whether there's a nested, aggregatable path + if (firstTry && !canBeAggregated(mapAccessor)) { + Value value = Values.value(Collections.singletonMap("_", mapAccessor.asMap(Function.identity()))); + return determineQueryRoot(value, rootNodeDescription, false); + } + } + + throw new NoRootNodeMappingException(mapAccessor, rootNodeDescription); + } + + private boolean canBeAggregated(MapAccessor mapAccessor) { + + if (mapAccessor instanceof Record r) { + return r.values().stream().anyMatch(this.pathType::isTypeOf); + } + return false; + } + + private boolean isSynthesized(MapAccessor mapAccessor) { + return mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) + && mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATIONS) + && mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); + } + + private Collection createDynamicLabelsProperty(TypeInformation type, Collection dynamicLabels) { + + Collection target = CollectionFactory.createCollection(type.getType(), String.class, + dynamicLabels.size()); + target.addAll(dynamicLabels); + return target; + } + + @Override + public void write(Object source, Map parameters) { + + Neo4jPersistentEntity nodeDescription = (Neo4jPersistentEntity) this.nodeDescriptionStore + .getNodeDescription(source.getClass()); + if (nodeDescription == null) { + return; + } + + Map properties = new HashMap<>(); + + if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) { + // add type info when write to the database + properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel()); + } + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); + PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> { + + // Skip the internal properties, we don't want them to end up stored as + // properties + if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() + || p.isReadOnly() || p.isVectorProperty()) { + return; + } + + final Value value = this.conversionService.writeValue(propertyAccessor.getProperty(p), + p.getTypeInformation(), p.getOptionalConverter()); + if (p.isComposite()) { + properties.put(p.getPropertyName(), new MapValueWrapper(value)); + } + else { + properties.put(p.getPropertyName(), value); + } + }); + + parameters.put(Constants.NAME_OF_PROPERTIES_PARAM, properties); + + // in case of relationship properties ignore internal id property + if (nodeDescription.hasIdProperty()) { + Neo4jPersistentProperty idProperty = nodeDescription.getRequiredIdProperty(); + parameters.put(Constants.NAME_OF_ID, + this.conversionService.writeValue(propertyAccessor.getProperty(idProperty), + idProperty.getTypeInformation(), idProperty.getOptionalConverter())); + } + // in case of relationship properties ignore internal id property + if (nodeDescription.hasVersionProperty()) { + Long versionProperty = (Long) propertyAccessor.getProperty(nodeDescription.getRequiredVersionProperty()); + + // we incremented this upfront the persist operation so the matching version + // would be one "before" + parameters.put(Constants.NAME_OF_VERSION_PARAM, versionProperty); + } + + // special handling for vector property to provide the needed procedure + // information + if (nodeDescription.hasVectorProperty()) { + Neo4jPersistentProperty vectorProperty = nodeDescription.getRequiredVectorProperty(); + parameters.put(Constants.NAME_OF_VECTOR_PROPERTY, vectorProperty.getPropertyName()); + parameters.put(Constants.NAME_OF_VECTOR_VALUE, + this.conversionService.writeValue(propertyAccessor.getProperty(vectorProperty), + vectorProperty.getTypeInformation(), vectorProperty.getOptionalConverter())); + } + } + /** - * @param queryResult The original query result or a reduced form like a node or similar - * @param allValues The original query result - * @param nodeDescription The node description of the current entity to be mapped from the result - * @param As in entity type - * @return The mapped entity + * Recursively maps an entity from the {@code queryResult} or the {@code allValues} + * accessor. + * @param queryResult the original query result or a reduced form like a node or + * similar + * @param allValues the original query result + * @param nodeDescription the node description of the current entity to be mapped from + * the result + * @param the entity type + * @return the mapped entity */ private ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity nodeDescription) { Collection relationshipsFromResult = extractRelationships(allValues); @@ -321,52 +409,64 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } @SuppressWarnings("unchecked") - private ET map(MapAccessor queryResult, Neo4jPersistentEntity nodeDescription, NodeDescription genericTargetNodeDescription, - @Nullable Object lastMappedEntity, @Nullable RelationshipDescription relationshipDescription, @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { + private ET map(MapAccessor queryResult, Neo4jPersistentEntity nodeDescription, + NodeDescription genericTargetNodeDescription, @Nullable Object lastMappedEntity, + @Nullable RelationshipDescription relationshipDescription, + @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { - // prior to SDN 7 local `getInternalId` didn't check relationships, so in that case, they have never been a known - // object. The centralized methods checks those too now. The condition is to recreate the old behaviour without - // losing the central access. The behaviour of knowObjects should take different sources of ids into account, + // prior to SDN 7 local `getInternalId` didn't check relationships, so in that + // case, they have never been a known + // object. The centralized methods checks those too now. The condition is to + // recreate the old behaviour without + // losing the central access. The behaviour of knowObjects should take different + // sources of ids into account, // as relationships and nodes might have overlapping values - String direction = relationshipDescription != null ? relationshipDescription.getDirection().name() : null; + String direction = (relationshipDescription != null) ? relationshipDescription.getDirection().name() : null; String internalId = IdentitySupport.getPrefixedElementId(queryResult, direction); Supplier mappedObjectSupplier = () -> { - knownObjects.setInCreation(internalId); + this.knownObjects.setInCreation(internalId); List allLabels = getLabels(queryResult, nodeDescription); - NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore - .deriveConcreteNodeDescription(nodeDescription, allLabels); + NodeDescriptionAndLabels nodeDescriptionAndLabels = this.nodeDescriptionStore + .deriveConcreteNodeDescription(nodeDescription, allLabels); @SuppressWarnings("unchecked") Neo4jPersistentEntity concreteNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels - .getNodeDescription(); + .getNodeDescription(); ET instance = instantiate(concreteNodeDescription, genericTargetNodeDescription, queryResult, - nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, relationshipsFromResult, nodesFromResult); + nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, relationshipsFromResult, + nodesFromResult); - knownObjects.removeFromInCreation(internalId); + this.knownObjects.removeFromInCreation(internalId); - populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false); + populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, + internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false); - var mostCurrentInstance = Objects.requireNonNull(getMostCurrentInstance(internalId, instance), "Could not get the most current instance for the internal id %s".formatted(internalId)); - PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(mostCurrentInstance); + var mostCurrentInstance = Objects.requireNonNull(getMostCurrentInstance(internalId, instance), + "Could not get the most current instance for the internal id %s".formatted(internalId)); + PersistentPropertyAccessor propertyAccessor = concreteNodeDescription + .getPropertyAccessor(mostCurrentInstance); ET bean = propertyAccessor.getBean(); - bean = eventSupport.maybeCallAfterConvert(bean, concreteNodeDescription, queryResult); + bean = this.eventSupport.maybeCallAfterConvert(bean, concreteNodeDescription, queryResult); // save final state of the bean - knownObjects.storeObject(internalId, bean); - knownObjects.mappedWithQueryResult(internalId, queryResult); + this.knownObjects.storeObject(internalId, bean); + this.knownObjects.mappedWithQueryResult(internalId, queryResult); return bean; }; @SuppressWarnings("unchecked") - ET mappedObject = (ET) knownObjects.getObject(internalId); + ET mappedObject = (ET) this.knownObjects.getObject(internalId); if (mappedObject == null) { mappedObject = mappedObjectSupplier.get(); - knownObjects.storeObject(internalId, mappedObject); - knownObjects.mappedWithQueryResult(internalId, queryResult); - } else if (knownObjects.alreadyMappedInPreviousRecord(internalId) || hasMoreFields(queryResult.asMap(), knownObjects.getQueryResultsFor(internalId))) { - // If the object were created in a run before or from a different path that represents another projection, + this.knownObjects.storeObject(internalId, mappedObject); + this.knownObjects.mappedWithQueryResult(internalId, queryResult); + } + else if (this.knownObjects.alreadyMappedInPreviousRecord(internalId) + || hasMoreFields(queryResult.asMap(), this.knownObjects.getQueryResultsFor(internalId))) { + // If the object were created in a run before or from a different path that + // represents another projection, // it _could_ have missing relationships and properties. // In such cases, we will add the additional data from the next record. // This can and should only work for @@ -374,10 +474,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { // AND (!!!) // 2. Mutable target types // because we cannot just create new instances - populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true); + populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, + internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true); } - // due to a needed side effect in `populateProperties`, the entity might have been changed - return Objects.requireNonNull(getMostCurrentInstance(internalId, mappedObject), "Could not get mapped instance for internal id %s".formatted(internalId)); + // due to a needed side effect in `populateProperties`, the entity might have been + // changed + return Objects.requireNonNull(getMostCurrentInstance(internalId, mappedObject), + "Could not get mapped instance for internal id %s".formatted(internalId)); } private boolean hasMoreFields(Map currentQueryResult, Set> savedQueryResults) { @@ -395,22 +498,24 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } @SuppressWarnings("unchecked") - @Nullable - private ET getMostCurrentInstance(@Nullable String internalId, @Nullable ET fallbackInstance) { - return (ET) (internalId != null && knownObjects.getObject(internalId) != null ? knownObjects.getObject(internalId) : fallbackInstance); + @Nullable private ET getMostCurrentInstance(@Nullable String internalId, @Nullable ET fallbackInstance) { + return (ET) ((internalId != null && this.knownObjects.getObject(internalId) != null) + ? this.knownObjects.getObject(internalId) : fallbackInstance); } - private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity baseNodeDescription, Neo4jPersistentEntity moreConcreteNodeDescription, @Nullable String internalId, - ET mappedObject, @Nullable Object lastMappedEntity, - @Nullable Collection relationshipsFromResult, Collection nodesFromResult, boolean objectAlreadyMapped) { + private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity baseNodeDescription, + Neo4jPersistentEntity moreConcreteNodeDescription, @Nullable String internalId, ET mappedObject, + @Nullable Object lastMappedEntity, @Nullable Collection relationshipsFromResult, + Collection nodesFromResult, boolean objectAlreadyMapped) { List allLabels = getLabels(queryResult, moreConcreteNodeDescription); - NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore - .deriveConcreteNodeDescription(moreConcreteNodeDescription, allLabels); + NodeDescriptionAndLabels nodeDescriptionAndLabels = this.nodeDescriptionStore + .deriveConcreteNodeDescription(moreConcreteNodeDescription, allLabels); @SuppressWarnings("unchecked") - Neo4jPersistentEntity concreteNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity) nodeDescriptionAndLabels - .getNodeDescription(), "Couldn't find required node description"); + Neo4jPersistentEntity concreteNodeDescription = Objects.requireNonNull( + (Neo4jPersistentEntity) nodeDescriptionAndLabels.getNodeDescription(), + "Couldn't find required node description"); if (!concreteNodeDescription.requiresPropertyPopulation()) { return; @@ -425,62 +530,72 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType()); // Fill simple properties PropertyHandler<@NonNull Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor, - isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType, objectAlreadyMapped); + isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType, + objectAlreadyMapped); PropertyHandlerSupport.of(concreteNodeDescription).doWithProperties(handler); - // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we - // reference the start again. Because it is getting still constructed, it won't be in the knownObjects + // in a cyclic graph / with bidirectional relationships, we could end up in a + // state in which we + // reference the start again. Because it is getting still constructed, it won't be + // in the knownObjects // store unless we temporarily put it there. - knownObjects.storeObject(internalId, propertyAccessor.getBean()); - knownObjects.mappedWithQueryResult(internalId, queryResult); + this.knownObjects.storeObject(internalId, propertyAccessor.getBean()); + this.knownObjects.mappedWithQueryResult(internalId, queryResult); - AssociationHandlerSupport.of(concreteNodeDescription).doWithAssociations( - populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult)); + AssociationHandlerSupport.of(concreteNodeDescription) + .doWithAssociations(populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, + objectAlreadyMapped, relationshipsFromResult, nodesFromResult)); } private Neo4jPersistentEntity getMostConcreteTargetNodeDescription( Neo4jPersistentEntity genericTargetNodeDescription, MapAccessor possibleValueNode) { List allLabels = getLabels(possibleValueNode, null); - NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore - .deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels); - return (Neo4jPersistentEntity) nodeDescriptionAndLabels - .getNodeDescription(); + NodeDescriptionAndLabels nodeDescriptionAndLabels = this.nodeDescriptionStore + .deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels); + return (Neo4jPersistentEntity) nodeDescriptionAndLabels.getNodeDescription(); } /** - * Returns the list of labels for the entity to be created from the "main" node returned. - * In case of a relationship that maps to a relationship properties definition, - * return the optional persisted type. - * - * @param queryResult The complete query result - * @return The list of labels defined by the query variable {@link Constants#NAME_OF_LABELS}. + * Returns the list of labels for the entity to be created from the "main" node + * returned. In case of a relationship that maps to a relationship properties + * definition, return the optional persisted type. + * @param queryResult the complete query result + * @param nodeDescription what are we working on + * @return the list of labels defined by the query variable + * {@link Constants#NAME_OF_LABELS}. */ private List getLabels(MapAccessor queryResult, @Nullable NodeDescription nodeDescription) { Value labelsValue = queryResult.get(Constants.NAME_OF_LABELS); List labels = new ArrayList<>(); if (!labelsValue.isNull()) { labels = labelsValue.asList(Value::asString); - } else if (queryResult instanceof Node nodeRepresentation) { + } + else if (queryResult instanceof Node nodeRepresentation) { nodeRepresentation.labels().forEach(labels::add); - } else if (queryResult instanceof Relationship) { + } + else if (queryResult instanceof Relationship) { Value value = queryResult.get(Constants.NAME_OF_RELATIONSHIP_TYPE); if (value.isNull() && nodeDescription != null) { labels.addAll(nodeDescription.getStaticLabels()); - } else { + } + else { labels.add(value.asString()); } - } else if (containsOnePlainNode(queryResult)) { + } + else if (containsOnePlainNode(queryResult)) { for (Value value : queryResult.values()) { - if (value.hasType(nodeType)) { + if (value.hasType(this.nodeType)) { Node node = value.asNode(); for (String label : node.labels()) { labels.add(label); } } } - } else if (!queryResult.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).isNull()) { + } + else if (!queryResult.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).isNull()) { queryResult.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asNode().labels().forEach(labels::add); - } else if (nodeDescription != null) { + } + else if (nodeDescription != null) { labels.addAll(nodeDescription.getStaticLabels()); } return labels; @@ -488,63 +603,79 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private boolean containsOnePlainNode(MapAccessor queryResult) { return StreamSupport.stream(queryResult.values().spliterator(), false) - .filter(value -> value.hasType(nodeType)).count() == 1L; + .filter(value -> value.hasType(this.nodeType)) + .count() == 1L; } - private ET instantiate(Neo4jPersistentEntity nodeDescription, NodeDescription genericNodeDescription, MapAccessor values, - Collection surplusLabels, @Nullable Object lastMappedEntity, + private ET instantiate(Neo4jPersistentEntity nodeDescription, NodeDescription genericNodeDescription, + MapAccessor values, Collection surplusLabels, @Nullable Object lastMappedEntity, @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { ParameterValueProvider<@NonNull Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<>() { @SuppressWarnings("unchecked") - // Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives + // Needed for the last cast. It's easier that way than using the parameter + // type info and checking for primitives @Override - @Nullable - public T getParameterValue(Parameter parameter) { - Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); + @Nullable public T getParameterValue(Parameter parameter) { + Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty( + Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); Object result; if (matchingProperty.isRelationship()) { - RelationshipDescription relationshipDescription = nodeDescription.getRelationships().stream() - .filter(r -> { - String propertyFieldName = matchingProperty.getFieldName(); - return r.getFieldName().equals(propertyFieldName); - }).findFirst().orElseThrow(); + RelationshipDescription relationshipDescription = nodeDescription.getRelationships() + .stream() + .filter(r -> { + String propertyFieldName = matchingProperty.getFieldName(); + return r.getFieldName().equals(propertyFieldName); + }) + .findFirst() + .orElseThrow(); // If we cannot find any value it does not mean that there isn't any. - // The result set might contain associations not named CONCRETE_TYPE_TARGET but ABSTRACT_TYPE_TARGET. + // The result set might contain associations not named + // CONCRETE_TYPE_TARGET but ABSTRACT_TYPE_TARGET. // For this we bubble up the hierarchy of NodeDescriptions. - result = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, genericNodeDescription, relationshipsFromResult, nodesFromResult) - .orElseGet(() -> { - NodeDescription parentNodeDescription = nodeDescription.getParentNodeDescription(); - T resultValue = null; - while (parentNodeDescription != null) { - Optional value = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, parentNodeDescription, relationshipsFromResult, nodesFromResult); - if (value.isPresent()) { - resultValue = (T) value.get(); - break; - } - parentNodeDescription = parentNodeDescription.getParentNodeDescription(); + result = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, + genericNodeDescription, relationshipsFromResult, nodesFromResult) + .orElseGet(() -> { + NodeDescription parentNodeDescription = nodeDescription.getParentNodeDescription(); + T resultValue = null; + while (parentNodeDescription != null) { + Optional value = createInstanceOfRelationships(matchingProperty, values, + relationshipDescription, parentNodeDescription, relationshipsFromResult, + nodesFromResult); + if (value.isPresent()) { + resultValue = (T) value.get(); + break; } - return resultValue; - }); - } else if (matchingProperty.isDynamicLabels()) { + parentNodeDescription = parentNodeDescription.getParentNodeDescription(); + } + return resultValue; + }); + } + else if (matchingProperty.isDynamicLabels()) { result = createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels); - } else if (matchingProperty.isEntityWithRelationshipProperties()) { + } + else if (matchingProperty.isEntityWithRelationshipProperties()) { result = lastMappedEntity; - } else { - result = conversionService.readValue(extractValueOf(matchingProperty, values), parameter.getType(), matchingProperty.getOptionalConverter()); + } + else { + result = DefaultNeo4jEntityConverter.this.conversionService.readValue( + extractValueOf(matchingProperty, values), parameter.getType(), + matchingProperty.getOptionalConverter()); } return (T) result; } }; - return entityInstantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider); + return this.entityInstantiators.getInstantiatorFor(nodeDescription) + .createInstance(nodeDescription, parameterValueProvider); } private PropertyHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, - PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, - Collection surplusLabels, @Nullable Object targetNode, boolean ownerIsKotlinType, boolean objectAlreadyMapped) { + PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, + Collection surplusLabels, @Nullable Object targetNode, boolean ownerIsKotlinType, + boolean objectAlreadyMapped) { return property -> { if (isConstructorParameter.test(property)) { @@ -554,16 +685,17 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { TypeInformation typeInformation = property.getTypeInformation(); if (!objectAlreadyMapped) { if (property.isDynamicLabels()) { - propertyAccessor.setProperty(property, - createDynamicLabelsProperty(typeInformation, surplusLabels)); - } else if (property.isAnnotationPresent(TargetNode.class)) { + propertyAccessor.setProperty(property, createDynamicLabelsProperty(typeInformation, surplusLabels)); + } + else if (property.isAnnotationPresent(TargetNode.class)) { if (queryResult instanceof Relationship) { propertyAccessor.setProperty(property, targetNode); } } } if (!property.isDynamicLabels() && !property.isAnnotationPresent(TargetNode.class)) { - Object value = conversionService.readValue(extractValueOf(property, queryResult), typeInformation, property.getOptionalConverter()); + Object value = this.conversionService.readValue(extractValueOf(property, queryResult), typeInformation, + property.getOptionalConverter()); if (value != null) { Class rawType = typeInformation.getType(); propertyAccessor.setProperty(property, getValueOrDefault(ownerIsKotlinType, rawType, value)); @@ -572,15 +704,10 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { }; } - private static Object getValueOrDefault(boolean ownerIsKotlinType, Class rawType, Object value) { - - return value == null && !ownerIsKotlinType && rawType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(rawType) : value; - } - - private AssociationHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, NodeDescription baseDescription, - PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, - boolean objectAlreadyMapped, @Nullable Collection relationshipsFromResult, Collection nodesFromResult - ) { + private AssociationHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, + NodeDescription baseDescription, PersistentPropertyAccessor propertyAccessor, + Predicate isConstructorParameter, boolean objectAlreadyMapped, + @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { return association -> { @@ -603,25 +730,31 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { if (propertyValue != null) { - boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike() && !((Collection) propertyValue).isEmpty(); - boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap() && !((Map) propertyValue).isEmpty(); - boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() && !persistentProperty.isMap(); + boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike() + && !((Collection) propertyValue).isEmpty(); + boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap() + && !((Map) propertyValue).isEmpty(); + boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() + && !persistentProperty.isMap(); if (populatedCollection) { - createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false) - .ifPresent(value -> { - Collection providedCollection = (Collection) value; - Collection existingValue = (Collection) propertyValue; - Collection newValue = CollectionFactory.createCollection(existingValue.getClass(), providedCollection.size() + existingValue.size()); + createInstanceOfRelationships(persistentProperty, queryResult, + (RelationshipDescription) association, baseDescription, relationshipsFromResult, + nodesFromResult, false) + .ifPresent(value -> { + Collection providedCollection = (Collection) value; + Collection existingValue = (Collection) propertyValue; + Collection newValue = CollectionFactory.createCollection(existingValue.getClass(), + providedCollection.size() + existingValue.size()); - RelationshipDescription relationshipDescription = (RelationshipDescription) association; - Map mergedValues = new HashMap<>(); - mergeCollections(relationshipDescription, existingValue, mergedValues); - mergeCollections(relationshipDescription, providedCollection, mergedValues); + RelationshipDescription relationshipDescription = (RelationshipDescription) association; + Map mergedValues = new HashMap<>(); + mergeCollections(relationshipDescription, existingValue, mergedValues); + mergeCollections(relationshipDescription, providedCollection, mergedValues); - newValue.addAll(mergedValues.values()); - propertyAccessor.setProperty(persistentProperty, newValue); - }); + newValue.addAll(mergedValues.values()); + propertyAccessor.setProperty(persistentProperty, newValue); + }); } boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue; @@ -632,48 +765,57 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } } - createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult) - .ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value)); + createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, + baseDescription, relationshipsFromResult, nodesFromResult) + .ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value)); }; } - private void mergeCollections(RelationshipDescription relationshipDescription, Collection values, Map mergedValues) { + private void mergeCollections(RelationshipDescription relationshipDescription, Collection values, + Map mergedValues) { for (Object existingValueInCollection : values) { if (relationshipDescription.hasRelationshipProperties()) { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription + .getRequiredRelationshipPropertiesEntity(); Object existingIdPropertyValue = relationshipPropertiesEntity - .getPropertyAccessor(existingValueInCollection) - .getProperty(relationshipPropertiesEntity.getRequiredIdProperty()); + .getPropertyAccessor(existingValueInCollection) + .getProperty(relationshipPropertiesEntity.getRequiredIdProperty()); mergedValues.put(existingIdPropertyValue, existingValueInCollection); - } else if (!relationshipDescription.isDynamic()) { // should not happen because this is all inside populatedCollection (but better safe than sorry) + } + else if (!relationshipDescription.isDynamic()) { // should not happen because + // this is all inside + // populatedCollection + // (but better safe than + // sorry) Neo4jPersistentEntity target = (Neo4jPersistentEntity) relationshipDescription.getTarget(); - Object existingIdPropertyValue = target - .getPropertyAccessor(existingValueInCollection) - .getProperty(target.getRequiredIdProperty()); + Object existingIdPropertyValue = target.getPropertyAccessor(existingValueInCollection) + .getProperty(target.getRequiredIdProperty()); mergedValues.put(existingIdPropertyValue, existingValueInCollection); } } } - private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, @Nullable Collection relationshipsFromResult, - Collection nodesFromResult) { - return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, relationshipsFromResult, nodesFromResult, true); + private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, + MapAccessor values, RelationshipDescription relationshipDescription, NodeDescription baseDescription, + @Nullable Collection relationshipsFromResult, Collection nodesFromResult) { + return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, + relationshipsFromResult, nodesFromResult, true); } @SuppressWarnings("deprecation") - private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, @Nullable Collection relationshipsFromResult, - Collection nodesFromResult, boolean fetchMore) { + private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, + MapAccessor values, RelationshipDescription relationshipDescription, NodeDescription baseDescription, + @Nullable Collection relationshipsFromResult, Collection nodesFromResult, + boolean fetchMore) { String typeOfRelationship = relationshipDescription.getType(); String targetLabel = relationshipDescription.getTarget().getPrimaryLabel(); Neo4jPersistentEntity genericTargetNodeDescription = (Neo4jPersistentEntity) relationshipDescription - .getTarget(); + .getTarget(); List value = new ArrayList<>(); Map dynamicValue = new HashMap<>(); @@ -682,8 +824,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Function keyTransformer; Class componentType = persistentProperty.getComponentType(); if (persistentProperty.isDynamicAssociation() && (componentType != null && componentType.isEnum())) { - keyTransformer = f -> conversionService.convert(f, componentType); - } else { + keyTransformer = f -> this.conversionService.convert(f, componentType); + } + else { keyTransformer = Function.identity(); } if (persistentProperty.isDynamicOneToManyAssociation()) { @@ -692,12 +835,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { mappedObjectHandler = (type, mappedObject) -> { @SuppressWarnings("unchecked") List bucket = (List) dynamicValue.computeIfAbsent(keyTransformer.apply(type), - s -> CollectionFactory.createCollection(actualType.getType(), persistentProperty.getAssociationTargetType(), values.size())); + s -> CollectionFactory.createCollection(actualType.getType(), + persistentProperty.getAssociationTargetType(), values.size())); bucket.add(mappedObject); }; - } else if (persistentProperty.isDynamicAssociation()) { + } + else if (persistentProperty.isDynamicAssociation()) { mappedObjectHandler = (type, mappedObject) -> dynamicValue.put(keyTransformer.apply(type), mappedObject); - } else { + } + else { mappedObjectHandler = (type, mappedObject) -> value.add(mappedObject); } @@ -719,22 +865,27 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { if (relationshipListEmptyOrNull && hasIdValue) { String sourceNodeId; Function sourceIdSelector; - Function targetIdSelector = relationshipDescription.isIncoming() ? Relationship::startNodeElementId : Relationship::endNodeElementId; + Function targetIdSelector = relationshipDescription.isIncoming() + ? Relationship::startNodeElementId : Relationship::endNodeElementId; if (elementId != null) { sourceNodeId = elementId; - sourceIdSelector = relationshipDescription.isIncoming() ? Relationship::endNodeElementId : Relationship::startNodeElementId; - } else { - // this can happen when someone used dto mapping and added the "classical" approach + sourceIdSelector = relationshipDescription.isIncoming() ? Relationship::endNodeElementId + : Relationship::startNodeElementId; + } + else { + // this can happen when someone used dto mapping and added the "classical" + // approach sourceNodeId = Long.toString(internalId); - Function hlp = relationshipDescription.isIncoming() ? Relationship::endNodeId : Relationship::startNodeId; + Function hlp = relationshipDescription.isIncoming() ? Relationship::endNodeId + : Relationship::startNodeId; sourceIdSelector = hlp.andThen(l -> Long.toString(l)); } // Retrieve all matching relationships from the result's list(s) - Collection allMatchingTypeRelationshipsInResult = - extractMatchingRelationships(relationshipsFromResult, relationshipDescription, typeOfRelationship, - (possibleRelationship) -> sourceIdSelector.apply(possibleRelationship).equals(sourceNodeId)); + Collection allMatchingTypeRelationshipsInResult = extractMatchingRelationships( + relationshipsFromResult, relationshipDescription, typeOfRelationship, + (possibleRelationship) -> sourceIdSelector.apply(possibleRelationship).equals(sourceNodeId)); // Fast exit if there is no relationship that can be mapped if (!allMatchingTypeRelationshipsInResult.isEmpty()) { @@ -744,114 +895,153 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { for (Node possibleValueNode : allNodesWithMatchingLabelInResult) { String targetNodeId = IdentitySupport.getElementId(possibleValueNode); - Neo4jPersistentEntity concreteTargetNodeDescription = - getMostConcreteTargetNodeDescription(genericTargetNodeDescription, possibleValueNode); + Neo4jPersistentEntity concreteTargetNodeDescription = getMostConcreteTargetNodeDescription( + genericTargetNodeDescription, possibleValueNode); Set relationshipsProcessed = new HashSet<>(); for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) { - if (targetIdSelector.apply(possibleRelationship).equals(targetNodeId)) { - - // Reduce the amount of relationships in the candidate list. - // If this relationship got processed twice (OUTGOING, INCOMING), it is never needed again - // and therefor should not be in the list. - // Otherwise, for highly linked data it could potentially cause a StackOverflowError. - String direction = relationshipDescription.getDirection().name(); - if (relationshipsFromResult != null && knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) { - relationshipsFromResult.remove(possibleRelationship); - } - // If the target is the same(equal) node, get the related object from the cache. - // Avoiding the call to the map method also breaks an endless cycle of trying to finish - // the property population of _this_ object. - // The initial population will happen at the end of this mapping. This is sufficient because - // it only affects properties not changing the instance of the object. - Object mappedObject; - if (fetchMore) { - mappedObject = sourceNodeId != null && sourceNodeId.equals(targetNodeId) - ? knownObjects.getObject("N" + sourceNodeId) - : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, relationshipsFromResult, nodesFromResult); - } else { - Object objectFromStore = knownObjects.getObject("N" + targetNodeId); - mappedObject = objectFromStore != null - ? objectFromStore - : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, relationshipsFromResult, nodesFromResult); - } - - if (relationshipDescription.hasRelationshipProperties()) { - Object relationshipProperties; - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); - if (fetchMore) { - relationshipProperties = map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult); - } else { - Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(possibleRelationship, relationshipDescription.getDirection().name())); - relationshipProperties = objectFromStore != null - ? objectFromStore - : map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult); - } - relationshipsAndProperties.add(relationshipProperties); - mappedObjectHandler.accept(possibleRelationship.type(), relationshipProperties); - } else { - mappedObjectHandler.accept(possibleRelationship.type(), mappedObject); - } - relationshipsProcessed.add(possibleRelationship); + if (!targetIdSelector.apply(possibleRelationship).equals(targetNodeId)) { + continue; } + + // Reduce the amount of relationships in the candidate list. + // If this relationship got processed twice (OUTGOING, + // INCOMING), it is never needed again + // and therefor should not be in the list. + // Otherwise, for highly linked data it could potentially + // cause a StackOverflowError. + String direction = relationshipDescription.getDirection().name(); + if (relationshipsFromResult != null && this.knownObjects.hasProcessedRelationshipCompletely( + "R" + direction + IdentitySupport.getElementId(possibleRelationship))) { + relationshipsFromResult.remove(possibleRelationship); + } + // If the target is the same(equal) node, get the related + // object from the cache. + // Avoiding the call to the map method also breaks an endless + // cycle of trying to finish + // the property population of _this_ object. + // The initial population will happen at the end of this + // mapping. This is sufficient because + // it only affects properties not changing the instance of the + // object. + Object mappedObject; + if (fetchMore) { + mappedObject = (sourceNodeId != null && sourceNodeId.equals(targetNodeId)) + ? this.knownObjects.getObject("N" + sourceNodeId) + : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, + relationshipsFromResult, nodesFromResult); + } + else { + Object objectFromStore = this.knownObjects.getObject("N" + targetNodeId); + mappedObject = (objectFromStore != null) ? objectFromStore + : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, + relationshipsFromResult, nodesFromResult); + } + + if (relationshipDescription.hasRelationshipProperties()) { + Object relationshipProperties; + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription + .getRequiredRelationshipPropertiesEntity(); + if (fetchMore) { + relationshipProperties = map(possibleRelationship, relationshipPropertiesEntity, + relationshipPropertiesEntity, mappedObject, relationshipDescription, + relationshipsFromResult, nodesFromResult); + } + else { + Object objectFromStore = this.knownObjects + .getObject(IdentitySupport.getPrefixedElementId(possibleRelationship, + relationshipDescription.getDirection().name())); + relationshipProperties = (objectFromStore != null) ? objectFromStore + : map(possibleRelationship, relationshipPropertiesEntity, + relationshipPropertiesEntity, mappedObject, relationshipDescription, + relationshipsFromResult, nodesFromResult); + } + relationshipsAndProperties.add(relationshipProperties); + mappedObjectHandler.accept(possibleRelationship.type(), relationshipProperties); + } + else { + mappedObjectHandler.accept(possibleRelationship.type(), mappedObject); + } + relationshipsProcessed.add(possibleRelationship); } allMatchingTypeRelationshipsInResult.removeAll(relationshipsProcessed); } } - } else if (!relationshipListEmptyOrNull) { + } + else if (!relationshipListEmptyOrNull) { for (Value relatedEntity : list.asList(Function.identity())) { - Neo4jPersistentEntity concreteTargetNodeDescription = - getMostConcreteTargetNodeDescription(genericTargetNodeDescription, relatedEntity); + Neo4jPersistentEntity concreteTargetNodeDescription = getMostConcreteTargetNodeDescription( + genericTargetNodeDescription, relatedEntity); Object valueEntry; if (fetchMore) { - valueEntry = map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, null, relationshipsFromResult, nodesFromResult); - } else { - Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(relatedEntity, null)); - valueEntry = objectFromStore != null - ? objectFromStore - : map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, null, relationshipsFromResult, nodesFromResult); + valueEntry = map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, + null, relationshipsFromResult, nodesFromResult); + } + else { + Object objectFromStore = this.knownObjects + .getObject(IdentitySupport.getPrefixedElementId(relatedEntity, null)); + valueEntry = (objectFromStore != null) ? objectFromStore + : map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, + null, relationshipsFromResult, nodesFromResult); } if (relationshipDescription.hasRelationshipProperties()) { - String sourceLabel = relationshipDescription.getSource().getMostAbstractParentLabel(baseDescription); - String relationshipSymbolicName = sourceLabel - + RelationshipDescription.NAME_OF_RELATIONSHIP + targetLabel; + String sourceLabel = relationshipDescription.getSource() + .getMostAbstractParentLabel(baseDescription); + String relationshipSymbolicName = sourceLabel + RelationshipDescription.NAME_OF_RELATIONSHIP + + targetLabel; Relationship relatedEntityRelationship = relatedEntity.get(relationshipSymbolicName) - .asRelationship(); + .asRelationship(); Object relationshipProperties; - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription.getRequiredRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationshipDescription + .getRequiredRelationshipPropertiesEntity(); if (fetchMore) { - relationshipProperties = map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult); - } else { - Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(relatedEntityRelationship, relationshipDescription.getDirection().name())); - relationshipProperties = objectFromStore != null - ? objectFromStore - : map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult); + relationshipProperties = map(relatedEntityRelationship, relationshipPropertiesEntity, + relationshipPropertiesEntity, valueEntry, relationshipDescription, + relationshipsFromResult, nodesFromResult); + } + else { + Object objectFromStore = this.knownObjects.getObject(IdentitySupport.getPrefixedElementId( + relatedEntityRelationship, relationshipDescription.getDirection().name())); + relationshipProperties = (objectFromStore != null) ? objectFromStore + : map(relatedEntityRelationship, relationshipPropertiesEntity, + relationshipPropertiesEntity, valueEntry, relationshipDescription, + relationshipsFromResult, nodesFromResult); } relationshipsAndProperties.add(relationshipProperties); - mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), relationshipProperties); - } else { - mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), + mappedObjectHandler.accept( + relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), + relationshipProperties); + } + else { + mappedObjectHandler.accept( + relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), valueEntry); } } } if (persistentProperty.getTypeInformation().isCollectionLike()) { - List returnedValues = relationshipDescription.hasRelationshipProperties() ? relationshipsAndProperties : value; - Collection target = CollectionFactory.createCollection(persistentProperty.getRawType(), componentType, returnedValues.size()); + List returnedValues = relationshipDescription.hasRelationshipProperties() + ? relationshipsAndProperties : value; + Collection target = CollectionFactory.createCollection(persistentProperty.getRawType(), + componentType, returnedValues.size()); target.addAll(returnedValues); return Optional.of(target); - } else { + } + else { if (relationshipDescription.isDynamic()) { return Optional.ofNullable(dynamicValue.isEmpty() ? null : dynamicValue); - } else if (relationshipDescription.hasRelationshipProperties()) { - return Optional.ofNullable(relationshipsAndProperties.isEmpty() ? null : relationshipsAndProperties.get(0)); - } else { + } + else if (relationshipDescription.hasRelationshipProperties()) { + return Optional + .ofNullable(relationshipsAndProperties.isEmpty() ? null : relationshipsAndProperties.get(0)); + } + else { return Optional.ofNullable(value.isEmpty() ? null : value.get(0)); } } @@ -859,103 +1049,71 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private Collection extractMatchingNodes(Collection allNodesInResult, String targetLabel) { - return labelNodeCache.computeIfAbsent(targetLabel, (label) -> { + return this.labelNodeCache.computeIfAbsent(targetLabel, (label) -> { Predicate onlyWithMatchingLabels = n -> n.hasLabel(label); - return allNodesInResult.stream() - .filter(onlyWithMatchingLabels) - .collect(Collectors.toList()); + return allNodesInResult.stream().filter(onlyWithMatchingLabels).collect(Collectors.toList()); }); } private Collection extractNodes(MapAccessor allValues) { Collection allNodesInResult = new LinkedHashSet<>(); StreamSupport.stream(allValues.values().spliterator(), false) - .filter(MappingSupport.isListContainingOnly(listType, this.nodeType)) - .flatMap(entry -> MappingSupport.extractNodesFromCollection(listType, entry).stream()) - .forEach(allNodesInResult::add); + .filter(MappingSupport.isListContainingOnly(this.listType, this.nodeType)) + .flatMap(entry -> MappingSupport.extractNodesFromCollection(this.listType, entry).stream()) + .forEach(allNodesInResult::add); StreamSupport.stream(allValues.values().spliterator(), false) - .filter(this.nodeType::isTypeOf) - .map(Value::asNode) - .forEach(allNodesInResult::add); + .filter(this.nodeType::isTypeOf) + .map(Value::asNode) + .forEach(allNodesInResult::add); return allNodesInResult; } - private Collection extractMatchingRelationships(@Nullable Collection relationshipsFromResult, - RelationshipDescription relationshipDescription, String typeOfRelationship, - Predicate relationshipPredicate) { + private Collection extractMatchingRelationships( + @Nullable Collection relationshipsFromResult, RelationshipDescription relationshipDescription, + String typeOfRelationship, Predicate relationshipPredicate) { - Predicate onlyWithMatchingType = r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic(); + Predicate onlyWithMatchingType = r -> r.type().equals(typeOfRelationship) + || relationshipDescription.isDynamic(); return (relationshipsFromResult != null) ? relationshipsFromResult.stream() - .filter(onlyWithMatchingType.and(relationshipPredicate)) - .collect(Collectors.toList()) : List.of(); + .filter(onlyWithMatchingType.and(relationshipPredicate)) + .collect(Collectors.toList()) : List.of(); } private Collection extractRelationships(MapAccessor allValues) { Collection allRelationshipsInResult = new LinkedHashSet<>(); StreamSupport.stream(allValues.values().spliterator(), false) - .filter(MappingSupport.isListContainingOnly(listType, this.relationshipType)) - .flatMap(entry -> MappingSupport.extractRelationshipsFromCollection(listType, entry).stream()) - .forEach(allRelationshipsInResult::add); + .filter(MappingSupport.isListContainingOnly(this.listType, this.relationshipType)) + .flatMap(entry -> MappingSupport.extractRelationshipsFromCollection(this.listType, entry).stream()) + .forEach(allRelationshipsInResult::add); StreamSupport.stream(allValues.values().spliterator(), false) - .filter(this.relationshipType::isTypeOf) - .map(Value::asRelationship) - .forEach(allRelationshipsInResult::add); + .filter(this.relationshipType::isTypeOf) + .map(Value::asRelationship) + .forEach(allRelationshipsInResult::add); return allRelationshipsInResult; } - @SuppressWarnings("deprecation") - private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) { - if (property.isInternalIdProperty()) { - if (Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(property.getType())) { - return Values.value(IdentitySupport.getInternalId(propertyContainer)); - } - return Values.value(IdentitySupport.getElementId(propertyContainer)); - } else if (property.isComposite()) { - String prefix = property.computePrefixWithDelimiter(); - - if (propertyContainer.containsKey(Constants.NAME_OF_ALL_PROPERTIES)) { - return extractCompositePropertyValues(propertyContainer.get(Constants.NAME_OF_ALL_PROPERTIES), prefix); - } else { - return extractCompositePropertyValues(propertyContainer, prefix); - } - } else { - String graphPropertyName = property.getPropertyName(); - if (propertyContainer.containsKey(graphPropertyName)) { - return propertyContainer.get(graphPropertyName); - } else if (propertyContainer.containsKey(Constants.NAME_OF_ALL_PROPERTIES)) { - return propertyContainer.get(Constants.NAME_OF_ALL_PROPERTIES).get(graphPropertyName); - } else { - return Values.NULL; - } - } - } - - private static Value extractCompositePropertyValues(MapAccessor propertyContainer, String prefix) { - Map hlp = new HashMap<>(propertyContainer.size()); - propertyContainer.keys().forEach(k -> { - if (k.startsWith(prefix)) { - hlp.put(k, propertyContainer.get(k)); - } - }); - return Values.value(hlp); - } - static class KnownObjects { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Lock read = lock.readLock(); - private final Lock write = lock.writeLock(); + + private final Lock read = this.lock.readLock(); + + private final Lock write = this.lock.writeLock(); private final Map internalIdStore = new HashMap<>(); + private final Map internalCurrentRecord = new HashMap<>(); + private final Set previousRecords = new HashSet<>(); + private final Set idsInCreation = new HashSet<>(); private final Map processedRelationships = new HashMap<>(); + private final Map>> mappedQueryResults = new HashMap<>(); private void storeObject(@Nullable String internalId, Object object) { @@ -963,12 +1121,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return; } try { - write.lock(); - idsInCreation.remove(internalId); - internalIdStore.put(internalId, object); - internalCurrentRecord.put(internalId, false); - } finally { - write.unlock(); + this.write.lock(); + this.idsInCreation.remove(internalId); + this.internalIdStore.put(internalId, object); + this.internalCurrentRecord.put(internalId, false); + } + finally { + this.write.unlock(); } } @@ -977,10 +1136,11 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return; } try { - write.lock(); - idsInCreation.add(internalId); - } finally { - write.unlock(); + this.write.lock(); + this.idsInCreation.add(internalId); + } + finally { + this.write.unlock(); } } @@ -989,41 +1149,41 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return false; } try { - read.lock(); - return idsInCreation.contains(internalId); - } finally { - read.unlock(); + this.read.lock(); + return this.idsInCreation.contains(internalId); + } + finally { + this.read.unlock(); } } private boolean containsNode(Node node) { try { - read.lock(); - return internalIdStore.containsKey(IdentitySupport.getElementId(node)); - } finally { - read.unlock(); + this.read.lock(); + return this.internalIdStore.containsKey(IdentitySupport.getElementId(node)); + } + finally { + this.read.unlock(); } } - @Nullable - private Object getObject(@Nullable String internalId) { + @Nullable private Object getObject(@Nullable String internalId) { if (internalId == null) { return null; } try { - read.lock(); + this.read.lock(); if (isInCreation(internalId)) { - throw new MappingException( - String.format( - "The node with id %s has a logical cyclic mapping dependency; " + - "its creation caused the creation of another node that has a reference to this", - internalId.substring(1)) - ); + throw new MappingException(String.format( + "The node with id %s has a logical cyclic mapping dependency; " + + "its creation caused the creation of another node that has a reference to this", + internalId.substring(1))); } - return internalIdStore.get(internalId); - } finally { - read.unlock(); + return this.internalIdStore.get(internalId); + } + finally { + this.read.unlock(); } } @@ -1032,10 +1192,11 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return; } try { - write.lock(); - idsInCreation.remove(internalId); - } finally { - write.unlock(); + this.write.lock(); + this.idsInCreation.remove(internalId); + } + finally { + this.write.unlock(); } } @@ -1045,34 +1206,39 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } try { - read.lock(); + this.read.lock(); - return previousRecords.contains(internalId) || Optional.ofNullable(internalCurrentRecord.get(internalId)).orElse(Boolean.FALSE); + return this.previousRecords.contains(internalId) + || Optional.ofNullable(this.internalCurrentRecord.get(internalId)).orElse(Boolean.FALSE); - } finally { - read.unlock(); + } + finally { + this.read.unlock(); } } /** - * This method has an intended side effect. - * It increases the process count of relationships (mapped by their ids) - * AND checks if it was already processed twice (INCOMING/OUTGOING). + * This method has an intended side effect. It increases the process count of + * relationships (mapped by their ids) AND checks if it was already processed + * twice (INCOMING/OUTGOING). + * @param relationshipId the id of the relationship to check + * @return true if the relationship has been completely processed */ private boolean hasProcessedRelationshipCompletely(String relationshipId) { try { - write.lock(); + this.write.lock(); - int processedAmount = processedRelationships.computeIfAbsent(relationshipId, s -> 0); + int processedAmount = this.processedRelationships.computeIfAbsent(relationshipId, s -> 0); if (processedAmount == 2) { return true; } - processedRelationships.put(relationshipId, processedAmount + 1); + this.processedRelationships.put(relationshipId, processedAmount + 1); return false; - } finally { - write.unlock(); + } + finally { + this.write.unlock(); } } @@ -1080,18 +1246,19 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { * Mark all currently existing objects as mapped. */ private void nextRecord() { - previousRecords.addAll(internalCurrentRecord.keySet()); - internalCurrentRecord.clear(); + this.previousRecords.addAll(this.internalCurrentRecord.keySet()); + this.internalCurrentRecord.clear(); } private void mappedWithQueryResult(@Nullable String internalId, MapAccessor queryResult) { if (internalId != null) { try { - write.lock(); - mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet()) - .add(queryResult.asMap()); - } finally { - write.unlock(); + this.write.lock(); + this.mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet()) + .add(queryResult.asMap()); + } + finally { + this.write.unlock(); } } } @@ -1101,11 +1268,14 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return Set.of(); } try { - read.lock(); - return Objects.requireNonNullElseGet(mappedQueryResults.get(internalId), Set::of); - } finally { - read.unlock(); + this.read.lock(); + return Objects.requireNonNullElseGet(this.mappedQueryResults.get(internalId), Set::of); + } + finally { + this.read.unlock(); } } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java index a54a5741a..fa8213a6a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -19,27 +19,31 @@ import java.util.Objects; import java.util.function.Function; import org.apache.commons.logging.LogFactory; + import org.springframework.core.log.LogAccessor; import org.springframework.data.support.IsNewStrategy; import org.springframework.util.Assert; /** - * Implementation of a {@link IsNewStrategy} that follows our supported identifiers and generators. Entities will be - * treated as new: + * Implementation of a {@link IsNewStrategy} that follows our supported identifiers and + * generators. Entities will be treated as new: *
    - *
  • when using internally generated (database) ids and the id property is {@literal null} or of a numeric primitive - * less than or equal {@literal 0},
  • + *
  • when using internally generated (database) ids and the id property is + * {@literal null} or of a numeric primitive less than or equal {@literal 0},
  • *
  • when using externally generated values and the id is {@literal null},
  • - *
  • when using assigned values without a version property or with a version property that is {@literal null}.
  • + *
  • when using assigned values without a version property or with a version property + * that is {@literal null}.
  • *
*

* An entity will not be treated as new *

    - *
  • when using internally generated (database) ids and the id property has a non-null value greater than - * {@literal 0},
  • - *
  • when using externally generated values and the id property is not {@literal null},
  • - *
  • when using assigned values together with {@link org.springframework.data.annotation.Version @Version} which has - * already a value not equal to {@literal null} or {@literal 0}.
  • + *
  • when using internally generated (database) ids and the id property has a non-null + * value greater than {@literal 0},
  • + *
  • when using externally generated values and the id property is not + * {@literal null},
  • + *
  • when using assigned values together with + * {@link org.springframework.data.annotation.Version @Version} which has already a value + * not equal to {@literal null} or {@literal 0}.
  • *
* * @author Michael J. Simons @@ -49,11 +53,25 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jIsNewStrategy.class)); + private final IdDescription idDescription; + + private final Class valueType; + + private final Function valueLookup; + + private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class valueType, + Function valueLookup) { + this.idDescription = idDescription; + this.valueType = valueType; + this.valueLookup = valueLookup; + } + static IsNewStrategy basedOn(Neo4jPersistentEntity entityMetaData) { Assert.notNull(entityMetaData, "Entity meta data must not be null"); - IdDescription idDescription = Objects.requireNonNull(entityMetaData.getIdDescription(), () -> "Cannot determine id description for entity %s".formatted(entityMetaData.getType())); + IdDescription idDescription = Objects.requireNonNull(entityMetaData.getIdDescription(), + () -> "Cannot determine id description for entity %s".formatted(entityMetaData.getType())); Class valueType = entityMetaData.getRequiredIdProperty().getType(); if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) { @@ -69,52 +87,40 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { + " with an assigned id will always be treated as new without version property"); valueType = Void.class; valueLookup = source -> null; - } else { + } + else { valueType = versionProperty.getType(); valueLookup = source -> entityMetaData.getPropertyAccessor(source).getProperty(versionProperty); } - } else { + } + else { valueLookup = source -> entityMetaData.getIdentifierAccessor(source).getIdentifier(); } return new DefaultNeo4jIsNewStrategy(idDescription, valueType, valueLookup); } - private final IdDescription idDescription; - - private final Class valueType; - - private final Function valueLookup; - - private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class valueType, - Function valueLookup) { - this.idDescription = idDescription; - this.valueType = valueType; - this.valueLookup = valueLookup; - } - - /* - * (non-Javadoc) - * @see IsNewStrategy#isNew(Object) - */ @Override public boolean isNew(Object entity) { - Object value = valueLookup.apply(entity); - if (idDescription.isInternallyGeneratedId()) { + Object value = this.valueLookup.apply(entity); + if (this.idDescription.isInternallyGeneratedId()) { boolean isNew; - if (value != null && valueType.isPrimitive() && value instanceof Number) { + if (value != null && this.valueType.isPrimitive() && value instanceof Number) { isNew = ((Number) value).longValue() < 0; - } else { + } + else { isNew = value == null; } return isNew; - } else if (idDescription.isExternallyGeneratedId()) { + } + else if (this.idDescription.isExternallyGeneratedId()) { return value == null; - } else if (idDescription.isAssignedId()) { - if (valueType != null && !valueType.isPrimitive()) { + } + else if (this.idDescription.isAssignedId()) { + if (this.valueType != null && !this.valueType.isPrimitive()) { return value == null; } @@ -123,8 +129,9 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy { } } - throw new IllegalArgumentException( - String.format("Could not determine whether %s is new! Unsupported identifier or version property", entity)); + throw new IllegalArgumentException(String + .format("Could not determine whether %s is new! Unsupported identifier or version property", entity)); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 0565279e7..bf071d61b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -35,6 +35,7 @@ import java.util.stream.Stream; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.log.LogAccessor; import org.springframework.data.annotation.Persistent; @@ -56,6 +57,9 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** + * Default implementation of the {@link Neo4jPersistentEntity}. + * + * @param type of the entity * @author Michael J. Simons * @author Gerrit Meier * @since 6.0 @@ -63,7 +67,10 @@ import org.springframework.util.StringUtils; final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity implements Neo4jPersistentEntity { - private static final Set> VALID_GENERATED_ID_TYPES = Stream.concat(Stream.of(String.class), DEPRECATED_GENERATED_ID_TYPES.stream()).collect(Collectors.toUnmodifiableSet()); + private static final Set> VALID_GENERATED_ID_TYPES = Stream + .concat(Stream.of(String.class), DEPRECATED_GENERATED_ID_TYPES.stream()) + .collect(Collectors.toUnmodifiableSet()); + private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jPersistentEntity.class)); /** @@ -86,35 +93,75 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity isRelationshipPropertiesEntity; + private final Lazy vectorProperty; + @Nullable private NodeDescription parentNodeDescription; private List> childNodeDescriptionsInHierarchy; - private final Lazy vectorProperty; - DefaultNeo4jPersistentEntity(TypeInformation information) { super(information); this.primaryLabel = computePrimaryLabel(this.getType()); this.additionalLabels = Lazy.of(this::computeAdditionalLabels); this.graphProperties = Lazy.of(this::computeGraphProperties); - this.dynamicLabelsProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast) - .filter(Neo4jPersistentProperty::isDynamicLabels).findFirst().orElse(null)); + this.dynamicLabelsProperty = Lazy.of(() -> getGraphProperties().stream() + .map(Neo4jPersistentProperty.class::cast) + .filter(Neo4jPersistentProperty::isDynamicLabels) + .findFirst() + .orElse(null)); this.isRelationshipPropertiesEntity = Lazy.of(() -> isAnnotationPresent(RelationshipProperties.class)); this.idDescription = Lazy.of(this::computeIdDescription); this.childNodeDescriptionsInHierarchy = computeChildNodeDescriptionInHierarchy(); - this.vectorProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast) - .filter(Neo4jPersistentProperty::isVectorProperty).findFirst().orElse(null)); + this.vectorProperty = Lazy.of(() -> getGraphProperties().stream() + .map(Neo4jPersistentProperty.class::cast) + .filter(Neo4jPersistentProperty::isVectorProperty) + .findFirst() + .orElse(null)); } - /* - * (non-Javadoc) - * @see NodeDescription#getPrimaryLabel() + /** + * The primary label will get computed and returned by following rules:
+ * 1. If there is no {@link Node} annotation, use the class name.
+ * 2. If there is an annotation but it has no properties set, use the class name.
+ * 3. If only {@link Node#labels()} property is set, use the first one as the primary + * label 4. If the {@link Node#primaryLabel()} property is set, use this as the + * primary label + * @param type the type of the underlying class + * @return computed primary label */ + static String computePrimaryLabel(Class type) { + + Node nodeAnnotation = AnnotatedElementUtils.findMergedAnnotation(type, Node.class); + if ((nodeAnnotation == null || hasEmptyLabelInformation(nodeAnnotation))) { + return type.getSimpleName(); + } + else if (StringUtils.hasText(nodeAnnotation.primaryLabel())) { + return nodeAnnotation.primaryLabel(); + } + else { + return nodeAnnotation.labels()[0]; + } + } + + /** + * Checks if an entity is explicitly annotated. + * @param entity the entity to check for annotation + * @return true if the type is explicitly annotated as entity and as such eligible to + * contribute to the list of labels and required to be part of the label lookup. + */ + private static boolean isExplicitlyAnnotatedAsEntity(Neo4jPersistentEntity entity) { + return entity.isAnnotationPresent(Node.class) || entity.isAnnotationPresent(Persistent.class); + } + + private static boolean hasEmptyLabelInformation(Node nodeAnnotation) { + return nodeAnnotation.labels().length < 1 && !StringUtils.hasText(nodeAnnotation.primaryLabel()); + } + @Override public String getPrimaryLabel() { - return primaryLabel; + return this.primaryLabel; } @Override @@ -140,29 +187,16 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity getUnderlyingClass() { return getType(); } - /* - * (non-Javadoc) - * @see NodeDescription#getIdDescription() - */ @Override - @Nullable - public IdDescription getIdDescription() { + @Nullable public IdDescription getIdDescription() { return this.idDescription.getNullable(); } - /* - * (non-Javadoc) - * @see NodeDescription#getGraphProperties() - */ @Override public Collection getGraphProperties() { return this.graphProperties.get(); @@ -173,10 +207,6 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity getGraphProperty(String fieldName) { return Optional.ofNullable(this.getPersistentProperty(fieldName)); @@ -200,10 +230,6 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity String.format("Duplicate definition of propert%s %s in entity %s", - duplicates.size() == 1 ? "y" : "ies", duplicates, getUnderlyingClass())); + (duplicates.size() != 1) ? "ies" : "y", duplicates, getUnderlyingClass())); } private void verifyDynamicAssociations() { Set> targetEntities = new HashSet<>(); - AssociationHandlerSupport.of(this).doWithAssociations((Association<@NonNull Neo4jPersistentProperty> association) -> { - Neo4jPersistentProperty inverse = association.getInverse(); - if (inverse.isDynamicAssociation()) { - Relationship relationship = inverse.findAnnotation(Relationship.class); - Assert.state(relationship == null || relationship.type().isEmpty(), - () -> "Dynamic relationships cannot be used with a fixed type; omit @Relationship or use @Relationship(direction = " - + Optional.ofNullable(relationship).map(Relationship::direction).orElse(Relationship.Direction.OUTGOING).name() + ") without a type in " + this.getUnderlyingClass() + " on field " - + inverse.getFieldName()); + AssociationHandlerSupport.of(this) + .doWithAssociations((Association<@NonNull Neo4jPersistentProperty> association) -> { + Neo4jPersistentProperty inverse = association.getInverse(); + if (inverse.isDynamicAssociation()) { + Relationship relationship = inverse.findAnnotation(Relationship.class); + Assert.state(relationship == null || relationship.type().isEmpty(), + () -> "Dynamic relationships cannot be used with a fixed type; omit @Relationship or use @Relationship(direction = " + + Optional.ofNullable(relationship) + .map(Relationship::direction) + .orElse(Relationship.Direction.OUTGOING) + .name() + + ") without a type in " + this.getUnderlyingClass() + " on field " + + inverse.getFieldName()); - Assert.state(!targetEntities.contains(inverse.getAssociationTargetType()), - () -> this.getUnderlyingClass() + " already contains a dynamic relationship to " - + inverse.getAssociationTargetType() - + "; only one dynamic relationship between to entities is permitted"); - targetEntities.add(inverse.getAssociationTargetType()); - } - }); + Assert.state(!targetEntities.contains(inverse.getAssociationTargetType()), + () -> this.getUnderlyingClass() + " already contains a dynamic relationship to " + + inverse.getAssociationTargetType() + + "; only one dynamic relationship between to entities is permitted"); + targetEntities.add(inverse.getAssociationTargetType()); + } + }); } private void verifyAssociationsWithProperties() { @@ -282,10 +314,10 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity messageSupplier = () -> String.format( "The class `%s` for the properties of a relationship " + "is missing a property for the generated, internal ID (`@Id @GeneratedValue Long id` " - + "or `@Id @GeneratedValue String id`) " - + "which is needed for safely updating properties", + + "or `@Id @GeneratedValue String id`) " + "which is needed for safely updating properties", this.getUnderlyingClass().getName()); - Assert.state(this.getIdDescription() != null && this.getIdDescription().isInternallyGeneratedId(), messageSupplier); + Assert.state(this.getIdDescription() != null && this.getIdDescription().isInternallyGeneratedId(), + messageSupplier); } } @@ -300,8 +332,9 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity String.format("Property %s on %s must extends %s", - persistentProperty.getFieldName(), persistentProperty.getOwner().getType(), Collection.class.getName())); + Assert.state(persistentProperty.isCollectionLike(), + () -> String.format("Property %s on %s must extends %s", persistentProperty.getFieldName(), + persistentProperty.getOwner().getType(), Collection.class.getName())); }); Assert.state(namesOfPropertiesWithDynamicLabels.size() <= 1, @@ -317,53 +350,35 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity String.format("There are multiple fields of type %s in entity %s: %s", - Vector.class.toString(), this.getName(), foundVectorDefinition.stream().map(p -> p.getPropertyName()).toList())); + Assert.state(foundVectorDefinition.size() <= 1, + () -> String.format("There are multiple fields of type %s in entity %s: %s", Vector.class.toString(), + this.getName(), foundVectorDefinition.stream().map(p -> p.getPropertyName()).toList())); } /** - * The primary label will get computed and returned by following rules:
- * 1. If there is no {@link Node} annotation, use the class name.
- * 2. If there is an annotation but it has no properties set, use the class name.
- * 3. If only {@link Node#labels()} property is set, use the first one as the primary label 4. If the - * {@link Node#primaryLabel()} property is set, use this as the primary label - * - * @param type the type of the underlying class - * @return computed primary label - */ - static String computePrimaryLabel(Class type) { - - Node nodeAnnotation = AnnotatedElementUtils.findMergedAnnotation(type, Node.class); - if ((nodeAnnotation == null || hasEmptyLabelInformation(nodeAnnotation))) { - return type.getSimpleName(); - } else if (StringUtils.hasText(nodeAnnotation.primaryLabel())) { - return nodeAnnotation.primaryLabel(); - } else { - return nodeAnnotation.labels()[0]; - } - } - - /** - * Additional labels are the ones defined directly on the entity and all labels of the parent classes if existing. - * + * Additional labels are the ones defined directly on the entity and all labels of the + * parent classes if existing. * @return all additional labels. */ private List computeAdditionalLabels() { return Stream.concat(computeOwnAdditionalLabels().stream(), computeParentLabels().stream()) - .distinct() // In case the interfaces added a duplicate of the primary label. - .filter(v -> !getPrimaryLabel().equals(v)) - .collect(Collectors.toList()); + .distinct() // In case the interfaces added a duplicate of the primary label. + .filter(v -> !getPrimaryLabel().equals(v)) + .collect(Collectors.toList()); } /** * The additional labels will get computed and returned by following rules:
* 1. If there is no {@link Node} annotation, empty {@code String} array.
- * 2. If there is an annotation but it has no properties set, empty {@code String} array.
- * 3a. If only {@link Node#labels()} property is set, use the all but the first one as the additional labels.
- * 3b. If the {@link Node#primaryLabel()} property is set, use the all but the first one as the additional labels.
- * 4. If the class has any interfaces that are explicitly annotated with {@link Node}, we take all values from them. - * + * 2. If there is an annotation but it has no properties set, empty {@code String} + * array.
+ * 3a. If only {@link Node#labels()} property is set, use the all but the first one as + * the additional labels.
+ * 3b. If the {@link Node#primaryLabel()} property is set, use the all but the first + * one as the additional labels.
+ * 4. If the class has any interfaces that are explicitly annotated with {@link Node}, + * we take all values from them. * @return computed additional labels of the concrete class */ private List computeOwnAdditionalLabels() { @@ -373,8 +388,10 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity computeParentLabels() { List parentLabels = new ArrayList<>(); - Neo4jPersistentEntity parentNodeDescriptionCalculated = (Neo4jPersistentEntity) parentNodeDescription; + Neo4jPersistentEntity parentNodeDescriptionCalculated = (Neo4jPersistentEntity) this.parentNodeDescription; while (parentNodeDescriptionCalculated != null) { if (isExplicitlyAnnotatedAsEntity(parentNodeDescriptionCalculated)) { @@ -408,20 +426,12 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity) parentNodeDescriptionCalculated.getParentNodeDescription(); + parentNodeDescriptionCalculated = (Neo4jPersistentEntity) parentNodeDescriptionCalculated + .getParentNodeDescription(); } return parentLabels; } - /** - * @param entity The entity to check for annotation - * @return True if the type is explicitly annotated as entity and as such eligible to contribute to the list of labels - * and required to be part of the label lookup. - */ - private static boolean isExplicitlyAnnotatedAsEntity(Neo4jPersistentEntity entity) { - return entity.isAnnotationPresent(Node.class) || entity.isAnnotationPresent(Persistent.class); - } - @Override public boolean describesInterface() { return this.getTypeInformation().getRawTypeInformation().getType().isInterface(); @@ -433,10 +443,12 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity getRelationships() { final List relationships = new ArrayList<>(); - AssociationHandlerSupport.of(this).doWithAssociations( - (Association association) -> relationships.add((RelationshipDescription) association)); + AssociationHandlerSupport.of(this) + .doWithAssociations((Association association) -> relationships + .add((RelationshipDescription) association)); return Collections.unmodifiableCollection(relationships); } - public Collection getRelationshipsInHierarchy(Predicate propertyFilter) { + @Override + public Collection getRelationshipsInHierarchy( + Predicate propertyFilter) { - return getRelationshipsInHierarchy(propertyFilter, PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass())); + return getRelationshipsInHierarchy(propertyFilter, + PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass())); } - public Collection getRelationshipsInHierarchy(Predicate propertyFilter, PropertyFilter.RelaxedPropertyPath path) { + Collection getRelationshipsInHierarchy( + Predicate propertyFilter, PropertyFilter.RelaxedPropertyPath path) { Collection relationships = new HashSet<>(getRelationships()); for (NodeDescription childDescription : getChildNodeDescriptionsInHierarchy()) { @@ -525,18 +538,21 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity target = concreteRelationship.getTarget(); - if (relationships.stream().noneMatch(relationship -> relationship.getFieldName().equals(fieldName) && relationship.getTarget().equals(target))) { + if (relationships.stream() + .noneMatch(relationship -> relationship.getFieldName().equals(fieldName) + && relationship.getTarget().equals(target))) { relationships.add(concreteRelationship); } }); } - return relationships.stream().filter(relationshipDescription -> - filterProperties(propertyFilter, relationshipDescription, path)) - .collect(Collectors.toSet()); + return relationships.stream() + .filter(relationshipDescription -> filterProperties(propertyFilter, relationshipDescription, path)) + .collect(Collectors.toSet()); } - private boolean filterProperties(Predicate propertyFilter, RelationshipDescription relationshipDescription, PropertyFilter.RelaxedPropertyPath path) { + private boolean filterProperties(Predicate propertyFilter, + RelationshipDescription relationshipDescription, PropertyFilter.RelaxedPropertyPath path) { PropertyFilter.RelaxedPropertyPath from = path.append(relationshipDescription.getFieldName()); return propertyFilter.test(from); } @@ -580,14 +596,15 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> getChildNodeDescriptionsInHierarchy() { - return childNodeDescriptionsInHierarchy; + return this.childNodeDescriptionsInHierarchy; } private List> computeChildNodeDescriptionInHierarchy() { - List> childNodes = new ArrayList<>(childNodeDescriptions); + List> childNodes = new ArrayList<>(this.childNodeDescriptions); - for (NodeDescription childNodeDescription : childNodeDescriptions) { - for (NodeDescription grantChildNodeDescription : childNodeDescription.getChildNodeDescriptionsInHierarchy()) { + for (NodeDescription childNodeDescription : this.childNodeDescriptions) { + for (NodeDescription grantChildNodeDescription : childNodeDescription + .getChildNodeDescriptionsInHierarchy()) { if (!childNodes.contains(grantChildNodeDescription)) { childNodes.add(grantChildNodeDescription); } @@ -596,16 +613,17 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity getParentNodeDescription() { + return this.parentNodeDescription; + } + @Override public void setParentNodeDescription(@Nullable NodeDescription parent) { this.parentNodeDescription = parent; } - @Nullable - public NodeDescription getParentNodeDescription() { - return parentNodeDescription; - } - @Override public boolean containsPossibleCircles(Predicate includeField) { return calculatePossibleCircles(includeField); @@ -616,11 +634,13 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> thisNodeVisited = Set.of(this); for (RelationshipDescription relationship : allRelationships) { - PropertyFilter.RelaxedPropertyPath relaxedPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass()); + PropertyFilter.RelaxedPropertyPath relaxedPropertyPath = PropertyFilter.RelaxedPropertyPath + .withRootType(this.getUnderlyingClass()); if (!filterProperties(includeField, relationship, relaxedPropertyPath)) { continue; } - // We don't look at the direction because we need to look for cycles based on the modelled relationship + // We don't look at the direction because we need to look for cycles based on + // the modelled relationship // direction instead of the "real graph" directions NodeDescription targetNode = relationship.getTarget(); if (this.equals(targetNode)) { @@ -631,16 +651,23 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> visitedNodes = new HashSet<>(thisNodeVisited); visitedNodes.add(targetNode); - // we don't care about the other content of relationship properties and jump straight into the `TargetNode` + // we don't care about the other content of relationship properties and jump + // straight into the `TargetNode` String relationshipPropertiesPrefix; if (!relationship.hasRelationshipProperties()) { relationshipPropertiesPrefix = ""; - } else { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); - var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + } + else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship + .getRequiredRelationshipPropertiesEntity(); + var targetNodeProperty = Objects.requireNonNull( + relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), + () -> "Could not get target node property on %s" + .formatted(relationshipPropertiesEntity.getType())); relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName(); } - PropertyFilter.RelaxedPropertyPath nextPath = relaxedPropertyPath.append(relationship.getFieldName() + relationshipPropertiesPrefix); + PropertyFilter.RelaxedPropertyPath nextPath = relaxedPropertyPath + .append(relationship.getFieldName() + relationshipPropertiesPrefix); if (calculatePossibleCircles(targetNode, visitedNodes, includeField, nextPath)) { return true; } @@ -648,8 +675,10 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity nodeDescription, Set> visitedNodes, Predicate includeField, PropertyFilter.RelaxedPropertyPath path) { - Collection allRelationships = new HashSet<>(((DefaultNeo4jPersistentEntity) nodeDescription).getRelationshipsInHierarchy(includeField, path)); + private boolean calculatePossibleCircles(NodeDescription nodeDescription, Set> visitedNodes, + Predicate includeField, PropertyFilter.RelaxedPropertyPath path) { + Collection allRelationships = new HashSet<>( + ((DefaultNeo4jPersistentEntity) nodeDescription).getRelationshipsInHierarchy(includeField, path)); Collection> visitedTargetNodes = new HashSet<>(); for (RelationshipDescription relationship : allRelationships) { @@ -662,17 +691,24 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> branchedVisitedNodes = new HashSet<>(visitedNodes); // Add the already visited target nodes for the next level, // but don't (!) add them to the visitedNodes yet. - // Otherwise, the same "parallel" defined target nodes will report a false circle. + // Otherwise, the same "parallel" defined target nodes will report a false + // circle. branchedVisitedNodes.add(targetNode); String relationshipPropertiesPrefix; if (!relationship.hasRelationshipProperties()) { relationshipPropertiesPrefix = ""; - } else { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); - var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + } + else { + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship + .getRequiredRelationshipPropertiesEntity(); + var targetNodeProperty = Objects.requireNonNull( + relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), + () -> "Could not get target node property on %s" + .formatted(relationshipPropertiesEntity.getType())); relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName(); } - if (calculatePossibleCircles(targetNode, branchedVisitedNodes, includeField, path.append(relationship.getFieldName() + relationshipPropertiesPrefix))) { + if (calculatePossibleCircles(targetNode, branchedVisitedNodes, includeField, + path.append(relationship.getFieldName() + relationshipPropertiesPrefix))) { return true; } } @@ -682,8 +718,7 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity graphPropertyName; + /** - * A flag whether this is a writeable property: Something that ends up on a Neo4j node or relationship. + * A flag whether this is a writeable property: Something that ends up on a Neo4j node + * or relationship. */ private final Lazy isWritableProperty; + /** * A flag whether this domain property manifests itself as a relationship in Neo4j. */ @@ -69,13 +75,15 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp /** * Creates a new {@link AnnotationBasedPersistentProperty}. - * - * @param property must not be {@literal null}. - * @param owner must not be {@literal null}. + * @param property must not be {@literal null} + * @param owner must not be {@literal null} + * @param mappingContext the mapping context in which this property is defined * @param simpleTypeHolder type holder + * @param optionalCharacteristics characteristics of this property */ DefaultNeo4jPersistentProperty(Property property, PersistentEntity owner, - Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, @Nullable PersistentPropertyCharacteristics optionalCharacteristics) { + Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, + @Nullable PersistentPropertyCharacteristics optionalCharacteristics) { super(property, owner, simpleTypeHolder); this.mappingContext = mappingContext; @@ -85,9 +93,14 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp this.isWritableProperty = Lazy.of(() -> { Class targetType = getActualType(); return simpleTypeHolder.isSimpleType(targetType) // The driver can do this - || this.mappingContext.hasCustomWriteTarget(targetType) // Some converter in the context can do this - || isAnnotationPresent(ConvertWith.class) // An explicit converter can do this - || isComposite(); // Our composite converter can do this + || this.mappingContext.hasCustomWriteTarget(targetType) // Some + // converter + // in the + // context can + // do this + || isAnnotationPresent(ConvertWith.class) // An explicit converter can + // do this + || isComposite(); // Our composite converter can do this }); this.isAssociation = Lazy.of(() -> { @@ -96,7 +109,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp if (isAnnotationPresent(Relationship.class)) { return true; } - return !(isWritableProperty.get()); + return !(this.isWritableProperty.get()); }); this.customConversion = Lazy.of(() -> { @@ -111,6 +124,33 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp this.optionalCharacteristics = optionalCharacteristics; } + static String deriveRelationshipType(String name) { + + Assert.hasText(name, "The name to derive the type from is required"); + + StringBuilder sb = new StringBuilder(); + + int codePoint; + int previousIndex = 0; + int i = 0; + while (i < name.length()) { + codePoint = name.codePointAt(i); + if (Character.isLowerCase(codePoint)) { + if (i > 0 && !Character.isLetter(name.codePointAt(previousIndex))) { + sb.append("_"); + } + codePoint = Character.toUpperCase(codePoint); + } + else if (sb.length() > 0) { + sb.append("_"); + } + sb.append(Character.toChars(codePoint)); + previousIndex = i; + i += Character.charCount(codePoint); + } + return sb.toString(); + } + @Override protected Association<@NonNull Neo4jPersistentProperty> createAssociation() { @@ -123,10 +163,13 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp if (this.hasActualTypeAnnotation(RelationshipProperties.class)) { TypeInformation typeInformation = getRelationshipPropertiesTargetType(getActualType()); obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow(); - relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())).orElseThrow(); - } else { + relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())) + .orElseThrow(); + } + else { Class associationTargetType = Objects.requireNonNull(this.getAssociationTargetType()); - obverseOwner = this.mappingContext.addPersistentEntity(TypeInformation.of(associationTargetType)).orElse(null); + obverseOwner = this.mappingContext.addPersistentEntity(TypeInformation.of(associationTargetType)) + .orElse(null); Assert.notNull(obverseOwner, "Obverse owner could not be added"); if (dynamicAssociation) { @@ -136,14 +179,16 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp TypeInformation actualType = mapValueType.getActualType(); if (actualType != null && this.mappingContext.getRequiredPersistentEntity(actualType.getType()) - .isRelationshipPropertiesEntity()) { + .isRelationshipPropertiesEntity()) { TypeInformation typeInformation = getRelationshipPropertiesTargetType(actualType.getType()); obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow(); - relationshipPropertiesClass = this.mappingContext - .addPersistentEntity(componentType).orElseThrow(); + relationshipPropertiesClass = this.mappingContext.addPersistentEntity(componentType) + .orElseThrow(); - } else if (mapValueType.getType().isAnnotationPresent(RelationshipProperties.class)) { - relationshipPropertiesClass = this.mappingContext.addPersistentEntity(componentType).orElseThrow(); + } + else if (mapValueType.getType().isAnnotationPresent(RelationshipProperties.class)) { + relationshipPropertiesClass = this.mappingContext.addPersistentEntity(componentType) + .orElseThrow(); } } } @@ -154,30 +199,34 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp String type; if (relationship != null && StringUtils.hasText(relationship.type())) { type = relationship.type(); - } else { + } + else { type = deriveRelationshipType(this.getName()); } - Relationship.Direction direction = relationship != null - ? relationship.direction() + Relationship.Direction direction = (relationship != null) ? relationship.direction() : Relationship.Direction.OUTGOING; - // Try to determine if there is a relationship definition that expresses logically the same relationship + // Try to determine if there is a relationship definition that expresses logically + // the same relationship // on the other end. // At this point, obverseOwner can't be null @SuppressWarnings("NullAway") - Optional obverseRelationshipDescription = obverseOwner.getRelationships().stream() - .filter(rel -> rel.getType().equals(type) - && rel.getTarget().equals(this.getOwner()) - && rel.getDirection() == direction.opposite()).findFirst(); + Optional obverseRelationshipDescription = obverseOwner.getRelationships() + .stream() + .filter(rel -> rel.getType().equals(type) && rel.getTarget().equals(this.getOwner()) + && rel.getDirection() == direction.opposite()) + .findFirst(); DefaultRelationshipDescription relationshipDescription = new DefaultRelationshipDescription(this, obverseRelationshipDescription.orElse(null), type, dynamicAssociation, (NodeDescription) getOwner(), - this.getName(), obverseOwner, direction, relationshipPropertiesClass, relationship == null || relationship.cascadeUpdates()); + this.getName(), obverseOwner, direction, relationshipPropertiesClass, + relationship == null || relationship.cascadeUpdates()); - // Update the previous found, if any, relationship with the newly created one as its counterpart. + // Update the previous found, if any, relationship with the newly created one as + // its counterpart. obverseRelationshipDescription - .ifPresent(observeRelationship -> observeRelationship.setRelationshipObverse(relationshipDescription)); + .ifPresent(observeRelationship -> observeRelationship.setRelationshipObverse(relationshipDescription)); return relationshipDescription; } @@ -191,8 +240,11 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp throw new MappingException("Missing @TargetNode declaration in " + relationshipPropertiesType); } TypeInformation relationshipPropertiesTypeInformation = TypeInformation.of(relationshipPropertiesType); - Class type = Objects.requireNonNull(relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName())).getType(); - if (Object.class == type && this.getRequiredField().getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) { + Class type = Objects + .requireNonNull(relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName())) + .getType(); + if (Object.class == type && this.getRequiredField().getGenericType() instanceof ParameterizedType pt + && pt.getActualTypeArguments().length == 1) { return TypeInformation.of(ResolvableType.forType(pt.getActualTypeArguments()[0])); } return TypeInformation.of(type); @@ -204,7 +256,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp if (isDynamicOneToManyAssociation()) { TypeInformation actualType = getTypeInformation().getRequiredActualType(); return actualType.getRequiredComponentType().getType(); - } else { + } + else { return getActualType(); } } @@ -217,7 +270,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp @Override public boolean isEntity() { - return super.isEntity() && !isWritableProperty.get() && !this.isAnnotationPresent(ConvertWith.class); + return super.isEntity() && !this.isWritableProperty.get() && !this.isAnnotationPresent(ConvertWith.class); } @Override @@ -232,27 +285,24 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp } @Override - @Nullable - public Neo4jPersistentPropertyConverter getOptionalConverter() { - return isEntity() ? null : customConversion.getOptional() - .map(Neo4jPersistentPropertyConverter.class::cast) - .orElse(null); + @Nullable public Neo4jPersistentPropertyConverter getOptionalConverter() { + return isEntity() ? null + : this.customConversion.getOptional().map(Neo4jPersistentPropertyConverter.class::cast).orElse(null); } /** * Computes the target name of this property. - * - * @return A property on a node or {@literal null} if this property describes an association. + * @return a property on a node or {@literal null} if this property describes an + * association */ - @Nullable - private String computeGraphPropertyName() { + @Nullable private String computeGraphPropertyName() { if (this.isRelationship()) { return null; } org.springframework.data.neo4j.core.schema.Property propertyAnnotation = this - .findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); + .findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); String targetName = this.getName(); if (propertyAnnotation != null && !propertyAnnotation.name().trim().isEmpty()) { @@ -296,46 +346,22 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp return isAnnotationPresent(CompositeProperty.class); } - static String deriveRelationshipType(String name) { - - Assert.hasText(name, "The name to derive the type from is required"); - - StringBuilder sb = new StringBuilder(); - - int codePoint; - int previousIndex = 0; - int i = 0; - while (i < name.length()) { - codePoint = name.codePointAt(i); - if (Character.isLowerCase(codePoint)) { - if (i > 0 && !Character.isLetter(name.codePointAt(previousIndex))) { - sb.append("_"); - } - codePoint = Character.toUpperCase(codePoint); - } else if (sb.length() > 0) { - sb.append("_"); - } - sb.append(Character.toChars(codePoint)); - previousIndex = i; - i += Character.charCount(codePoint); - } - return sb.toString(); - } - @Override public boolean isReadOnly() { - if (optionalCharacteristics != null && optionalCharacteristics.isReadOnly() != null) { - return Boolean.TRUE.equals(optionalCharacteristics.isReadOnly()); + if (this.optionalCharacteristics != null && this.optionalCharacteristics.isReadOnly() != null) { + return Boolean.TRUE.equals(this.optionalCharacteristics.isReadOnly()); } Class typeOfAnnotation = org.springframework.data.neo4j.core.schema.Property.class; - return isAnnotationPresent(ReadOnlyProperty.class) || (isAnnotationPresent(typeOfAnnotation) && getRequiredAnnotation(typeOfAnnotation).readOnly()); + return isAnnotationPresent(ReadOnlyProperty.class) + || (isAnnotationPresent(typeOfAnnotation) && getRequiredAnnotation(typeOfAnnotation).readOnly()); } @Override public boolean isTransient() { - return this.optionalCharacteristics == null || optionalCharacteristics.isTransient() == null ? - super.isTransient() : Boolean.TRUE.equals(optionalCharacteristics.isTransient()); + return (this.optionalCharacteristics == null || this.optionalCharacteristics.isTransient() == null) + ? super.isTransient() : Boolean.TRUE.equals(this.optionalCharacteristics.isTransient()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java index b1f5f8781..920e24e09 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java @@ -19,15 +19,20 @@ import java.util.Objects; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.Association; import org.springframework.data.neo4j.core.schema.Relationship; /** + * Default implementation of the Neo4j specific association + * {@link RelationshipDescription}. + * * @author Michael J. Simons * @author Gerrit Meier * @since 6.0 */ -final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPersistentProperty> implements RelationshipDescription { +final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPersistentProperty> + implements RelationshipDescription { private final String type; @@ -44,17 +49,18 @@ final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPer @Nullable private final NodeDescription relationshipPropertiesClass; + private final boolean cascadeUpdates; + @Nullable private RelationshipDescription relationshipObverse; - private final boolean cascadeUpdates; + DefaultRelationshipDescription(Neo4jPersistentProperty inverse, + @Nullable RelationshipDescription relationshipObverse, String type, boolean dynamic, + NodeDescription source, String fieldName, NodeDescription target, Relationship.Direction direction, + @Nullable NodeDescription relationshipProperties, boolean cascadeUpdates) { - DefaultRelationshipDescription(Neo4jPersistentProperty inverse, @Nullable RelationshipDescription relationshipObverse, - String type, boolean dynamic, NodeDescription source, String fieldName, NodeDescription target, - Relationship.Direction direction, @Nullable NodeDescription relationshipProperties, - boolean cascadeUpdates) { - - // the immutable obverse association-wise is always null because we cannot determine them on both sides + // the immutable obverse association-wise is always null because we cannot + // determine them on both sides // if we consider to support bidirectional relationships. super(inverse, null); @@ -71,38 +77,37 @@ final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPer @Override public String getType() { - return type; + return this.type; } @Override public boolean isDynamic() { - return dynamic; + return this.dynamic; } @Override public NodeDescription getTarget() { - return target; + return this.target; } @Override public NodeDescription getSource() { - return source; + return this.source; } @Override public String getFieldName() { - return fieldName; + return this.fieldName; } @Override public Relationship.Direction getDirection() { - return direction; + return this.direction; } @Override - @Nullable - public NodeDescription getRelationshipPropertiesEntity() { - return relationshipPropertiesClass; + @Nullable public NodeDescription getRelationshipPropertiesEntity() { + return this.relationshipPropertiesClass; } @Override @@ -111,14 +116,13 @@ final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPer } @Override - public void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse) { - this.relationshipObverse = relationshipObverse; + @Nullable public RelationshipDescription getRelationshipObverse() { + return this.relationshipObverse; } @Override - @Nullable - public RelationshipDescription getRelationshipObverse() { - return relationshipObverse; + public void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse) { + this.relationshipObverse = relationshipObverse; } @Override @@ -128,13 +132,7 @@ final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPer @Override public boolean cascadeUpdates() { - return cascadeUpdates; - } - - @Override - public String toString() { - return "DefaultRelationshipDescription{" + "type='" + type + '\'' + ", source='" + source + '\'' + ", direction='" - + direction + '\'' + ", target='" + target + '}'; + return this.cascadeUpdates; } @Override @@ -142,16 +140,23 @@ final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPer if (this == o) { return true; } - if (!(o instanceof DefaultRelationshipDescription)) { + if (!(o instanceof DefaultRelationshipDescription that)) { return false; } - DefaultRelationshipDescription that = (DefaultRelationshipDescription) o; - return (isDynamic() ? getFieldName().equals(that.getFieldName()) : getType().equals(that.getType())) && getTarget().equals(that.getTarget()) - && getSource().equals(that.getSource()) && getDirection().equals(that.getDirection()); + return (isDynamic() ? getFieldName().equals(that.getFieldName()) : getType().equals(that.getType())) + && getTarget().equals(that.getTarget()) && getSource().equals(that.getSource()) + && getDirection().equals(that.getDirection()); } @Override public int hashCode() { - return Objects.hash(fieldName, type, target, source, direction); + return Objects.hash(this.fieldName, this.type, this.target, this.source, this.direction); } + + @Override + public String toString() { + return "DefaultRelationshipDescription{" + "type='" + this.type + '\'' + ", source='" + this.source + '\'' + + ", direction='" + this.direction + '\'' + ", target='" + this.target + '}'; + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DtoInstantiatingConverter.java index 261a6ff93..4598a3f35 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DtoInstantiatingConverter.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogAccessor; @@ -40,12 +41,11 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** - * {@link Converter} to instantiate DTOs from fully equipped domain objects. - * The original idea of this converter and it's usage is to be found in Spring Data Mongo. Thanks to the original - * authors Oliver Drotbohm and Mark Paluch. + * {@link Converter} to instantiate DTOs from fully equipped domain objects. The original + * idea of this converter and it's usage is to be found in Spring Data Mongo. Thanks to + * the original authors Oliver Drotbohm and Mark Paluch. * * @author Michael J. Simons - * @soundtrack Gustavo Santaolalla - The Last Of Us */ @API(status = API.Status.INTERNAL, since = "6.1.2") public final class DtoInstantiatingConverter implements Converter { @@ -53,11 +53,11 @@ public final class DtoInstantiatingConverter implements Converter targetType; + private final Neo4jMappingContext context; /** * Creates a new {@link Converter} to instantiate DTOs. - * * @param dtoType must not be {@literal null}. * @param context must not be {@literal null}. */ @@ -71,18 +71,16 @@ public final class DtoInstantiatingConverter implements Converter sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass()); + Neo4jPersistentEntity sourceEntity = this.context.getRequiredPersistentEntity(entityInstance.getClass()); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(entityInstance); - Neo4jPersistentEntity targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)).orElseThrow(() -> new IllegalStateException("Target entity could not be created for a DTO")); + Neo4jPersistentEntity targetEntity = this.context.addPersistentEntity(TypeInformation.of(this.targetType)) + .orElseThrow(() -> new IllegalStateException("Target entity could not be created for a DTO")); InstanceCreatorMetadata creator = targetEntity.getInstanceCreatorMetadata(); - Object dto = context.getInstantiatorFor(targetEntity) - .createInstance(targetEntity, - getParameterValueProvider( - targetEntity, - targetProperty -> getPropertyValueDirectlyFor(targetProperty, sourceEntity, sourceAccessor)) - ); + Object dto = this.context.getInstantiatorFor(targetEntity) + .createInstance(targetEntity, getParameterValueProvider(targetEntity, + targetProperty -> getPropertyValueDirectlyFor(targetProperty, sourceEntity, sourceAccessor))); PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); PropertyHandlerSupport.of(targetEntity).doWithProperties(property -> { @@ -98,9 +96,8 @@ public final class DtoInstantiatingConverter implements Converter targetProperty, PersistentEntity sourceEntity, - PersistentPropertyAccessor sourceAccessor) { + @Nullable Object getPropertyValueDirectlyFor(PersistentProperty targetProperty, PersistentEntity sourceEntity, + PersistentPropertyAccessor sourceAccessor) { String targetPropertyName = targetProperty.getName(); PersistentProperty sourceProperty = sourceEntity.getPersistentProperty(targetPropertyName); @@ -110,35 +107,34 @@ public final class DtoInstantiatingConverter implements Converter sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass()); + Neo4jPersistentEntity sourceEntity = this.context.getRequiredPersistentEntity(entityInstance.getClass()); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(entityInstance); - Neo4jPersistentEntity targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)) - .orElseThrow(() -> new MappingException( - "Could not add a persistent entity for the projection target type '" + targetType.getName() + "'")); - InstanceCreatorMetadata<@NonNull ? extends PersistentProperty> creator = targetEntity.getInstanceCreatorMetadata(); + Neo4jPersistentEntity targetEntity = this.context.addPersistentEntity(TypeInformation.of(this.targetType)) + .orElseThrow(() -> new MappingException("Could not add a persistent entity for the projection target type '" + + this.targetType.getName() + "'")); + InstanceCreatorMetadata<@NonNull ? extends PersistentProperty> creator = targetEntity + .getInstanceCreatorMetadata(); - Object dto = context.getInstantiatorFor(targetEntity) - .createInstance(targetEntity, - getParameterValueProvider( - targetEntity, - targetProperty -> getPropertyValueFor(targetProperty, sourceEntity, sourceAccessor, entityInstanceAndSource)) - ); + Object dto = this.context.getInstantiatorFor(targetEntity) + .createInstance(targetEntity, + getParameterValueProvider(targetEntity, targetProperty -> getPropertyValueFor(targetProperty, + sourceEntity, sourceAccessor, entityInstanceAndSource))); PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); targetEntity.doWithAll(property -> setPropertyOnDtoObject(entityInstanceAndSource, sourceEntity, sourceAccessor, @@ -148,11 +144,11 @@ public final class DtoInstantiatingConverter implements Converter getParameterValueProvider( - Neo4jPersistentEntity targetEntity, - Function extractFromSource - ) { + Neo4jPersistentEntity targetEntity, Function extractFromSource) { return new ParameterValueProvider<>() { - @SuppressWarnings("unchecked") // Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives + @SuppressWarnings("unchecked") // Needed for the last cast. It's easier that + // way than using the parameter type info and + // checking for primitives @Override public T getParameterValue(Parameter parameter) { String parameterName = parameter.getName(); @@ -163,7 +159,7 @@ public final class DtoInstantiatingConverter implements Converter sourceEntity, + @Nullable Object getPropertyValueFor(Neo4jPersistentProperty targetProperty, PersistentEntity sourceEntity, PersistentPropertyAccessor sourceAccessor, EntityInstanceWithSource entityInstanceAndSource) { TypeSystem typeSystem = entityInstanceAndSource.getTypeSystem(); @@ -197,18 +192,23 @@ public final class DtoInstantiatingConverter implements Converter String.format("" - + "Cannot retrieve a value for property `%s` of DTO `%s` and the property will always be null. " - + "Make sure to project only properties of the domain type or use a custom query that " - + "returns a mappable data under the name `%1$s`.", targetPropertyName, targetType.getName())); - } else if (targetProperty.isMap()) { - log.warn(() -> String.format("" - + "%s is an additional property to be projected. " - + "However, map properties cannot be projected and the property will always be null.", + log.warn(() -> String.format( + "" + "Cannot retrieve a value for property `%s` of DTO `%s` and the property will always be null. " + + "Make sure to project only properties of the domain type or use a custom query that " + + "returns a mappable data under the name `%1$s`.", + targetPropertyName, this.targetType.getName())); + } + else if (targetProperty.isMap()) { + log.warn(() -> String.format( + "" + "%s is an additional property to be projected. " + + "However, map properties cannot be projected and the property will always be null.", targetPropertyName)); - } else { - // We don't support associations on the top level of DTO projects which is somewhat inline with the restrictions - // regarding DTO projections as described in https://docs.spring.io/spring-data/jpa/docs/2.4.0-RC1/reference/html/#projections.dtos + } + else { + // We don't support associations on the top level of DTO projects which is + // somewhat inline with the restrictions + // regarding DTO projections as described in + // https://docs.spring.io/spring-data/jpa/docs/2.4.0-RC1/reference/html/#projections.dtos // > except that no proxying happens and no nested projections can be applied // Therefore, we extract associations kinda half-manual. @@ -217,24 +217,28 @@ public final class DtoInstantiatingConverter implements Converter String.format("" + "%s is a list property but the selected value is not a list and the property will always be null.", targetPropertyName)); - } else { + } + else { Class actualType = targetProperty.getActualType(); Function singleValue; - if (context.hasPersistentEntityFor(actualType)) { - singleValue = p -> context.getEntityConverter().read(actualType, p); - } else { + if (this.context.hasPersistentEntityFor(actualType)) { + singleValue = p -> this.context.getEntityConverter().read(actualType, p); + } + else { TypeInformation actualTargetType = TypeInformation.of(actualType); - singleValue = p -> context.getConversionService().readValue(p, actualTargetType, targetProperty.getOptionalConverter()); + singleValue = p -> this.context.getConversionService() + .readValue(p, actualTargetType, targetProperty.getOptionalConverter()); } if (targetProperty.isCollectionLike()) { List returnedValues = property.asList(singleValue); - Collection target = CollectionFactory - .createCollection(targetProperty.getType(), actualType, returnedValues.size()); + Collection target = CollectionFactory.createCollection(targetProperty.getType(), actualType, + returnedValues.size()); target.addAll(returnedValues); return target; - } else { + } + else { return singleValue.apply(property); } } @@ -242,4 +246,5 @@ public final class DtoInstantiatingConverter implements Converter entity type + * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "6.1.2") public final class EntityFromDtoInstantiatingConverter implements Converter { private final Class targetEntityType; + private final Neo4jMappingContext context; private final Map, EntityFromDtoInstantiatingConverter> converterCache = new ConcurrentHashMap<>(); /** * Creates a new {@link Converter} to instantiate Entities from DTOs. - * * @param entityType must not be {@literal null}. - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. */ public EntityFromDtoInstantiatingConverter(Class entityType, Neo4jMappingContext context) { @@ -66,34 +68,36 @@ public final class EntityFromDtoInstantiatingConverter implements Converter sourceEntity = context.addPersistentEntity(TypeInformation.of(dtoInstance.getClass())) - .orElseThrow(); + PersistentEntity sourceEntity = this.context + .addPersistentEntity(TypeInformation.of(dtoInstance.getClass())) + .orElseThrow(); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance); - PersistentEntity targetEntity = Objects.requireNonNull(context.getPersistentEntity(targetEntityType)); + PersistentEntity targetEntity = Objects + .requireNonNull(this.context.getPersistentEntity(this.targetEntityType)); InstanceCreatorMetadata creator = Objects.requireNonNull(targetEntity.getInstanceCreatorMetadata()); @SuppressWarnings({ "rawtypes", "unchecked" }) - T entity = (T) context.getInstantiatorFor(targetEntity) - .createInstance(targetEntity, new ParameterValueProvider() { - @Override - @Nullable - public Object getParameterValue(Parameter parameter) { - PersistentProperty targetProperty = targetEntity.getPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); - if (targetProperty == null) { - throw new MappingException("Cannot map constructor parameter " + parameter.getName() - + " to a property of class " + targetEntityType); - } - return getPropertyValueFor(targetProperty, sourceEntity, sourceAccessor); + T entity = (T) this.context.getInstantiatorFor(targetEntity) + .createInstance(targetEntity, new ParameterValueProvider() { + @Override + @Nullable public Object getParameterValue(Parameter parameter) { + PersistentProperty targetProperty = targetEntity.getPersistentProperty( + Objects.requireNonNull(parameter.getName(), "Parameter names are not available")); + if (targetProperty == null) { + throw new MappingException( + "Cannot map constructor parameter " + parameter.getName() + " to a property of class " + + EntityFromDtoInstantiatingConverter.this.targetEntityType); } - }); + return getPropertyValueFor(targetProperty, sourceEntity, sourceAccessor); + } + }); PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(entity); targetEntity.doWithAll(property -> { @@ -107,8 +111,7 @@ public final class EntityFromDtoInstantiatingConverter implements Converter targetProperty, PersistentEntity sourceEntity, + @Nullable Object getPropertyValueFor(PersistentProperty targetProperty, PersistentEntity sourceEntity, PersistentPropertyAccessor sourceAccessor) { String targetPropertyName = targetProperty.getName(); @@ -123,8 +126,10 @@ public final class EntityFromDtoInstantiatingConverter implements Converter nestedConverter = converterCache.computeIfAbsent(targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, context)); + if (targetProperty.isAssociation() && !targetProperty.isAnnotationPresent(TargetNode.class) + && targetProperty.isCollectionLike()) { + EntityFromDtoInstantiatingConverter nestedConverter = this.converterCache.computeIfAbsent( + targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, this.context)); Collection source = (Collection) propertyValue; if (source == null) { return CollectionFactory.createCollection(targetPropertyType, 0); @@ -135,16 +140,18 @@ public final class EntityFromDtoInstantiatingConverter implements Converter nestedConverter = converterCache.computeIfAbsent(targetProperty.getType(), - t -> new EntityFromDtoInstantiatingConverter<>(t, context)); + } + else { + EntityFromDtoInstantiatingConverter nestedConverter = this.converterCache.computeIfAbsent( + targetProperty.getType(), t -> new EntityFromDtoInstantiatingConverter<>(t, this.context)); return nestedConverter.convert(propertyValue); } } return propertyValue; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityInstanceWithSource.java b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityInstanceWithSource.java index 316f3d315..d8002595a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityInstanceWithSource.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityInstanceWithSource.java @@ -22,16 +22,18 @@ import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; /** - * Used to keep the raw result around in case of a DTO based projection so that missing properties can be filled later on. + * Used to keep the raw result around in case of a DTO based projection so that missing + * properties can be filled later on. * * @author Michael J. Simons - * @soundtrack The Prodigy - Music For The Jilted Generation */ @API(status = API.Status.INTERNAL, since = "6.1.2") public final class EntityInstanceWithSource { /** - * An instance of the original {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity source entity} + * An instance of the original. + * {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity source + * entity} */ private final Object entityInstance; @@ -41,14 +43,11 @@ public final class EntityInstanceWithSource { private final TypeSystem typeSystem; /** - * The record from which the source above was hydrated and which might contain top level properties that are eligible to mapping. + * The record from which the source above was hydrated and which might contain top + * level properties that are eligible to mapping. */ private final MapAccessor sourceRecord; - public static BiFunction decorateMappingFunction(BiFunction target) { - return (t, r) -> new EntityInstanceWithSource(target.apply(t, r), t, r); - } - private EntityInstanceWithSource(Object entityInstance, TypeSystem typeSystem, MapAccessor sourceRecord) { this.entityInstance = entityInstance; @@ -56,15 +55,21 @@ public final class EntityInstanceWithSource { this.sourceRecord = sourceRecord; } + public static BiFunction decorateMappingFunction( + BiFunction target) { + return (t, r) -> new EntityInstanceWithSource(target.apply(t, r), t, r); + } + public Object getEntityInstance() { - return entityInstance; + return this.entityInstance; } public TypeSystem getTypeSystem() { - return typeSystem; + return this.typeSystem; } public MapAccessor getSourceRecord() { - return sourceRecord; + return this.sourceRecord; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/GraphPropertyDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/GraphPropertyDescription.java index ca982abfc..fc0dd2fab 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/GraphPropertyDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/GraphPropertyDescription.java @@ -18,10 +18,12 @@ package org.springframework.data.neo4j.core.mapping; import org.apiguardian.api.API; /** - * Provides minimal information how to map class attributes to the properties of a node or a relationship. + * Provides minimal information how to map class attributes to the properties of a node or + * a relationship. *

- * Spring Data's persistent properties have slightly different semantics. They have an entity centric approach of - * properties. Spring Data properties contain - if not marked otherwise - also associations. + * Spring Data's persistent properties have slightly different semantics. They have an + * entity centric approach of properties. Spring Data properties contain - if not marked + * otherwise - also associations. *

* Associations between different node types can be queried on the {@link Schema} itself. * @@ -32,39 +34,47 @@ import org.apiguardian.api.API; public interface GraphPropertyDescription { /** - * @return The name of the attribute of the mapped class + * The name of the attribute of the mapped class. + * @return the name of the attribute of the mapped class */ String getFieldName(); /** - * @return The name of the property as stored in the graph. + * The name of the property as stored in the graph. + * @return the name of the property as stored in the graph */ String getPropertyName(); /** - * @return True if this property is the id property. + * True if this property is the id property. + * @return true if this property is the id property */ boolean isIdProperty(); /** - * @return True, if this property is the id property and the owner uses internal ids. + * Flag, if this is an internal id property. + * @return true, if this property is the id property and the owner uses internal ids */ boolean isInternalIdProperty(); /** - * This will return the type of a simple property or the component type of a collection like property. - * - * @return The type of this property. + * This will return the type of a simple property or the component type of a + * collection like property. + * @return the type of this property. */ Class getActualType(); /** - * @return Whether this property describes a relationship or not. + * Flag, if this is a relationship. + * @return whether this property describes a relationship or not. */ boolean isRelationship(); /** - * @return True if the entity's property (this object) is stored as multiple properties on a node or relationship. + * Flag, if this is a composite property. + * @return true if the entity's property (this object) is stored as multiple + * properties on a node or relationship. */ boolean isComposite(); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java index fad47c6ca..1a4c6ee6b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/IdDescription.java @@ -23,6 +23,7 @@ import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.SymbolicName; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.data.util.Lazy; @@ -54,10 +55,33 @@ public final class IdDescription { */ @Nullable private final String graphPropertyName; + private final boolean isDeprecated; private final Lazy idExpression; + @SuppressWarnings("deprecation") + private IdDescription(SymbolicName symbolicName, @Nullable Class> idGeneratorClass, + @Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) { + + this.idGeneratorClass = idGeneratorClass; + this.idGeneratorRef = (idGeneratorRef != null && idGeneratorRef.isEmpty()) ? null : idGeneratorRef; + this.graphPropertyName = graphPropertyName; + this.isDeprecated = isDeprecated; + + this.idExpression = Lazy.of(() -> { + final Node rootNode = Cypher.anyNode(symbolicName); + if (this.isInternallyGeneratedId()) { + return isDeprecated ? rootNode.internalId() : rootNode.elementId(); + } + else { + return this.getOptionalGraphPropertyName() + .map(propertyName -> Cypher.property(symbolicName, propertyName)) + .get(); + } + }); + } + public static IdDescription forAssignedIds(SymbolicName symbolicName, String graphPropertyName) { Assert.notNull(graphPropertyName, "Graph property name is required"); @@ -73,15 +97,15 @@ public final class IdDescription { } public static IdDescription forExternallyGeneratedIds(SymbolicName symbolicName, - Class> idGeneratorClass, - String idGeneratorRef, String graphPropertyName) { + Class> idGeneratorClass, String idGeneratorRef, String graphPropertyName) { Assert.notNull(graphPropertyName, "Graph property name is required"); try { Assert.hasText(idGeneratorRef, "Reference to an ID generator has precedence"); return new IdDescription(symbolicName, null, idGeneratorRef, graphPropertyName, false); - } catch (IllegalArgumentException e) { + } + catch (IllegalArgumentException ex) { Assert.notNull(idGeneratorClass, "Class of id generator is required"); Assert.isTrue(idGeneratorClass != GeneratedValue.InternalIdGenerator.class, "Cannot use InternalIdGenerator for externally generated ids"); @@ -90,73 +114,57 @@ public final class IdDescription { } } - @SuppressWarnings("deprecation") - private IdDescription(SymbolicName symbolicName, @Nullable Class> idGeneratorClass, - @Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) { - - this.idGeneratorClass = idGeneratorClass; - this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef; - this.graphPropertyName = graphPropertyName; - this.isDeprecated = isDeprecated; - - this.idExpression = Lazy.of(() -> { - final Node rootNode = Cypher.anyNode(symbolicName); - if (this.isInternallyGeneratedId()) { - return isDeprecated ? rootNode.internalId() : rootNode.elementId(); - } else { - return this.getOptionalGraphPropertyName() - .map(propertyName -> Cypher.property(symbolicName, propertyName)).get(); - } - }); - } - public Expression asIdExpression() { return this.idExpression.get(); } /** - * Creates the right identifier expression for this node entity. - * Note: This enforces a recalculation of the name on invoke. - * + * Creates the right identifier expression for this node entity. Note: This enforces a + * recalculation of the name on invoke. * @param nodeName use this name as the symbolic name of the node in the query - * @return An expression that represents the right identifier type. + * @return an expression that represents the right identifier type */ @SuppressWarnings("deprecation") public Expression asIdExpression(String nodeName) { final Node rootNode = Cypher.anyNode(nodeName); if (this.isInternallyGeneratedId()) { - return isDeprecated ? rootNode.internalId() : rootNode.elementId(); - } else { + return this.isDeprecated ? rootNode.internalId() : rootNode.elementId(); + } + else { return this.getOptionalGraphPropertyName() - .map(propertyName -> Cypher.property(nodeName, propertyName)).orElseThrow(); + .map(propertyName -> Cypher.property(nodeName, propertyName)) + .orElseThrow(); } } public Optional>> getIdGeneratorClass() { - return Optional.ofNullable(idGeneratorClass); + return Optional.ofNullable(this.idGeneratorClass); } public Optional getIdGeneratorRef() { - return Optional.ofNullable(idGeneratorRef); + return Optional.ofNullable(this.idGeneratorRef); } /** - * @return True, if the ID is assigned to the entity before the entity hits the database, either manually or through a - * generator. + * Flag, if this is an assigned id. + * @return true, if the ID is assigned to the entity before the entity hits the + * database, either manually or through a generator. */ public boolean isAssignedId() { return this.idGeneratorClass == null && this.idGeneratorRef == null; } /** - * @return True, if the database generated the ID. + * Flag, if this is a database generated id. + * @return true, if the database generated the ID. */ public boolean isInternallyGeneratedId() { return this.idGeneratorClass == GeneratedValue.InternalIdGenerator.class; } /** - * @return True, if the ID is externally generated. + * Flag, if this is an externally generated id. + * @return true, if the ID is externally generated */ public boolean isExternallyGeneratedId() { return (this.idGeneratorClass != null && this.idGeneratorClass != GeneratedValue.InternalIdGenerator.class) @@ -164,13 +172,13 @@ public final class IdDescription { } /** - * An ID description has only a corresponding graph property name when it's bas on an external assigment. An internal - * id has no corresponding graph property and therefore this method will return an empty {@link Optional} in such - * cases. - * - * @return The name of an optional graph property. + * An ID description has only a corresponding graph property name when it's bas on an + * external assigment. An internal id has no corresponding graph property and + * therefore this method will return an empty {@link Optional} in such cases. + * @return the name of an optional graph property */ public Optional getOptionalGraphPropertyName() { - return Optional.ofNullable(graphPropertyName); + return Optional.ofNullable(this.graphPropertyName); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java index a469e6c03..f18886b9a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.apiguardian.api.API.Status.INTERNAL; - import java.util.function.Function; import org.apiguardian.api.API; @@ -28,14 +26,17 @@ import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.TypeSystem; +import static org.apiguardian.api.API.Status.INTERNAL; + /** - * This class is not part of any public API and will be changed without further notice as needed. It's - * primary goal is to mitigate the changes in Neo4j5, which introduces the notion of an {@literal element id} for both nodes - * and relationships while deprecating {@literal id} at the same time. The identity support allows to isolate our calls - * deprecated API in one central place and will exist for SDN 7 only to make SDN 7 work with both Neo4j 4.4 and Neo4j 5.x. + * This class is not part of any public API and will be changed without + * further notice as needed. It's primary goal is to mitigate the changes in Neo4j5, which + * introduces the notion of an {@literal element id} for both nodes and relationships + * while deprecating {@literal id} at the same time. The identity support allows to + * isolate our calls deprecated API in one central place and will exist for SDN 7 only to + * make SDN 7 work with both Neo4j 4.4 and Neo4j 5.x. * * @author Michael J. Simons - * @soundtrack Buckethead - SIGIL Soundtrack * @since 7.0 */ @API(status = INTERNAL) @@ -45,22 +46,21 @@ public final class IdentitySupport { } /** - * @param entity The entity container as received from the server. - * @return The internal id + * Retrieves the element id of an entity. + * @param entity the entity container as received from the server. + * @return the internal id */ public static String getElementId(Entity entity) { return entity.elementId(); } - /** - * Retrieves an identity either from attributes inside the row or if it is an actual entity, with the dedicated accessors. - * - * @param row A query result row - * @return An internal id + * Retrieves an identity either from attributes inside the row or if it is an actual + * entity, with the dedicated accessors. + * @param row a query result row + * @return an internal id */ - @Nullable - public static String getElementId(MapAccessor row) { + @Nullable public static String getElementId(MapAccessor row) { if (row instanceof Entity entity) { return getElementId(entity); } @@ -78,8 +78,7 @@ public final class IdentitySupport { @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated - @Nullable - public static Long getInternalId(MapAccessor row) { + @Nullable public static Long getInternalId(MapAccessor row) { if (row instanceof Entity entity) { return entity.id(); } @@ -92,13 +91,15 @@ public final class IdentitySupport { return row.get(columnToUse).asLong(); } - @Nullable - public static String getPrefixedElementId(MapAccessor queryResult, @Nullable String seed) { + @Nullable public static String getPrefixedElementId(MapAccessor queryResult, @Nullable String seed) { if (queryResult instanceof Node) { return "N" + getElementId(queryResult); - } else if (queryResult instanceof Relationship) { + } + else if (queryResult instanceof Relationship) { return "R" + seed + getElementId(queryResult); - } else if (!(queryResult.get(Constants.NAME_OF_ELEMENT_ID) == null || queryResult.get(Constants.NAME_OF_ELEMENT_ID).isNull())) { + } + else if (!(queryResult.get(Constants.NAME_OF_ELEMENT_ID) == null + || queryResult.get(Constants.NAME_OF_ELEMENT_ID).isNull())) { Value value = queryResult.get(Constants.NAME_OF_ELEMENT_ID); if (value.hasType(TypeSystem.getDefault().NUMBER())) { return "N" + value.asNumber(); @@ -110,7 +111,9 @@ public final class IdentitySupport { } public static Function mapperForRelatedIdValues(@Nullable Neo4jPersistentProperty idProperty) { - boolean deprecatedHolder = idProperty != null && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(idProperty.getType()); + boolean deprecatedHolder = idProperty != null + && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(idProperty.getType()); return deprecatedHolder ? IdentitySupport::getInternalId : IdentitySupport::getElementId; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/MapValueWrapper.java b/src/main/java/org/springframework/data/neo4j/core/mapping/MapValueWrapper.java index 795814f35..29e82939b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/MapValueWrapper.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/MapValueWrapper.java @@ -19,11 +19,12 @@ import org.apiguardian.api.API; import org.neo4j.driver.Value; /** - * A wrapper or marker for a Neo4j {@code org.neo4j.driver.internal.value.MapValue} that needs to be unwrapped when used - * for properties. - * This class exists solely for projection / filtering purposes: It allows the {@link DefaultNeo4jEntityConverter} to keep - * the composite properties together as long as possible (in the form of above's {@code MapValue}. Thus, the key in the - * {@link Constants#NAME_OF_PROPERTIES_PARAM} fits the filter so that we can continue filtering after binding. + * A wrapper or marker for a Neo4j {@code org.neo4j.driver.internal.value.MapValue} that + * needs to be unwrapped when used for properties. This class exists solely for projection + * / filtering purposes: It allows the {@link DefaultNeo4jEntityConverter} to keep the + * composite properties together as long as possible (in the form of above's + * {@code MapValue}. Thus, the key in the {@link Constants#NAME_OF_PROPERTIES_PARAM} fits + * the filter so that we can continue filtering after binding. * * @author Michael J. Simons */ @@ -37,6 +38,7 @@ public final class MapValueWrapper { } public Value getMapValue() { - return mapValue; + return this.mapValue; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java index 5ff442d67..7f19bf223 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java @@ -32,10 +32,13 @@ import org.neo4j.driver.Value; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.Type; + import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.schema.TargetNode; /** + * Utility methods for the actual object mapping. + * * @author Michael J. Simons * @author Philipp Tölle * @author Gerrit Meier @@ -44,14 +47,18 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @API(status = API.Status.INTERNAL, since = "6.0") public final class MappingSupport { + private MappingSupport() { + } + /** - * The value for a relationship can be a scalar object (1:1), a collection (1:n), a map (1:n, but with dynamic - * relationship types) or a map (1:n) with properties for each relationship. This method unifies the type into - * something iterable, depending on the given inverse type. - * - * @param rawValue The raw value to unify - * @return A unified collection (Either a collection of Map.Entry for dynamic and relationships with properties or a - * list of related values) + * The value for a relationship can be a scalar object (1:1), a collection (1:n), a + * map (1:n, but with dynamic relationship types) or a map (1:n) with properties for + * each relationship. This method unifies the type into something iterable, depending + * on the given inverse type. + * @param property the property that constitutes the relationship + * @param rawValue the raw value to unify + * @return a unified collection (Either a collection of Map.Entry for dynamic and + * relationships with properties or a list of related values) */ public static Collection unifyRelationshipValue(Neo4jPersistentProperty property, @Nullable Object rawValue) { @@ -62,43 +69,47 @@ public final class MappingSupport { Collection unifiedValue; if (property.isDynamicAssociation()) { if (property.isDynamicOneToManyAssociation()) { - unifiedValue = ((Map) rawValue) - .entrySet().stream() - .flatMap(e -> ((Collection) e.getValue()).stream().map(v -> new SimpleEntry<>(e.getKey(), v))) - .collect(Collectors.toList()); - } else { + unifiedValue = ((Map) rawValue).entrySet() + .stream() + .flatMap(e -> ((Collection) e.getValue()).stream().map(v -> new SimpleEntry<>(e.getKey(), v))) + .collect(Collectors.toList()); + } + else { unifiedValue = ((Map) rawValue).entrySet(); } - } else if (property.isCollectionLike()) { + } + else if (property.isCollectionLike()) { unifiedValue = (Collection) rawValue; - } else { + } + else { unifiedValue = Collections.singleton(rawValue); } return unifiedValue; } /** - * A helper that produces a predicate to check whether a {@link Value} is a list value and contains only other - * values with a given type. - * - * @param collectionType The required collection type system - * @param requiredType The required type - * @return A predicate + * A helper that produces a predicate to check whether a {@link Value} is a list value + * and contains only other values with a given type. + * @param collectionType the required collection type system + * @param requiredType the required type + * @return a2 predicate */ public static Predicate isListContainingOnly(Type collectionType, Type requiredType) { Predicate containsOnlyRequiredType = entry -> { - // either this is a list containing other list of possible the same required type + // either this is a list containing other list of possible the same required + // type // or the type exists directly in the list for (Value listEntry : entry.values()) { if (listEntry.hasType(collectionType)) { boolean listInListCorrectType = true; for (Value listInListEntry : entry.asList(Function.identity())) { - listInListCorrectType = listInListCorrectType && isListContainingOnly(collectionType, requiredType) - .test(listInListEntry); + listInListCorrectType = listInListCorrectType + && isListContainingOnly(collectionType, requiredType).test(listInListEntry); } return listInListCorrectType; - } else if (!listEntry.hasType(requiredType)) { + } + else if (!listEntry.hasType(requiredType)) { return false; } } @@ -116,11 +127,13 @@ public final class MappingSupport { for (Value listWithRelationshipsOrRelationship : entry.values()) { if (listWithRelationshipsOrRelationship.hasType(collectionType)) { relationships.addAll(listWithRelationshipsOrRelationship.asList(Value::asRelationship)); - } else { + } + else { relationships.add(listWithRelationshipsOrRelationship.asRelationship()); } } - } else { + } + else { relationships.add(entry.asRelationship()); } return relationships; @@ -135,95 +148,97 @@ public final class MappingSupport { for (Value listWithNodesOrNode : entry.values()) { if (listWithNodesOrNode.hasType(collectionType)) { nodes.addAll(listWithNodesOrNode.asList(Value::asNode)); - } else { + } + else { nodes.add(listWithNodesOrNode.asNode()); } } - } else { + } + else { nodes.add(entry.asNode()); } return nodes; } /** - * Extract the relationship properties or just the related object if there are no relationship properties - * attached. - * + * Extract the relationship properties or just the related object if there are no + * relationship properties attached. * @param neo4jMappingContext - current mapping context * @param hasRelationshipProperties - does this relationship has properties * @param isDynamicAssociation - is the defined relationship a dynamic association - * @param valueToStore - either a plain object or {@link RelationshipPropertiesWithEntityHolder} + * @param valueToStore - either a plain object or + * {@link RelationshipPropertiesWithEntityHolder} * @param propertyAccessor - PropertyAccessor for the value - * * @return extracted related object or relationship properties */ public static Object getRelationshipOrRelationshipPropertiesObject(Neo4jMappingContext neo4jMappingContext, - boolean hasRelationshipProperties, - boolean isDynamicAssociation, - Object valueToStore, - PersistentPropertyAccessor propertyAccessor) { + boolean hasRelationshipProperties, boolean isDynamicAssociation, Object valueToStore, + PersistentPropertyAccessor propertyAccessor) { Object newRelationshipObject = propertyAccessor.getBean(); if (hasRelationshipProperties) { - MappingSupport.RelationshipPropertiesWithEntityHolder entityHolder = - (RelationshipPropertiesWithEntityHolder) - (isDynamicAssociation - ? ((Map.Entry) valueToStore).getValue() - : valueToStore); + MappingSupport.RelationshipPropertiesWithEntityHolder entityHolder = (RelationshipPropertiesWithEntityHolder) (isDynamicAssociation + ? ((Map.Entry) valueToStore).getValue() : valueToStore); Object relationshipPropertiesValue = entityHolder.getRelationshipProperties(); - Neo4jPersistentEntity persistentEntity = - Objects.requireNonNull(neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass())); + Neo4jPersistentEntity persistentEntity = Objects + .requireNonNull(neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass())); - PersistentPropertyAccessor relationshipPropertiesAccessor = persistentEntity.getPropertyAccessor(relationshipPropertiesValue); - relationshipPropertiesAccessor.setProperty(Objects.requireNonNull(persistentEntity.getPersistentProperty(TargetNode.class)), newRelationshipObject); + PersistentPropertyAccessor relationshipPropertiesAccessor = persistentEntity + .getPropertyAccessor(relationshipPropertiesValue); + relationshipPropertiesAccessor.setProperty( + Objects.requireNonNull(persistentEntity.getPersistentProperty(TargetNode.class)), + newRelationshipObject); newRelationshipObject = relationshipPropertiesAccessor.getBean(); - // If we recreate or manipulate the object including it's accessor, we must update it in the holder as well. + // If we recreate or manipulate the object including it's accessor, we must + // update it in the holder as well. entityHolder.setRelationshipProperties(newRelationshipObject); } return newRelationshipObject; } - private MappingSupport() {} - /** - * Class that defines a tuple of relationship with properties and the connected target entity. + * Class that defines a tuple of relationship with properties and the connected target + * entity. */ @API(status = API.Status.INTERNAL) - public final static class RelationshipPropertiesWithEntityHolder { + public static final class RelationshipPropertiesWithEntityHolder { private final Neo4jPersistentEntity relationshipPropertiesEntity; - private PersistentPropertyAccessor relationshipPropertiesPropertyAccessor; - private Object relationshipProperties; + private final Object relatedEntity; - RelationshipPropertiesWithEntityHolder( - Neo4jPersistentEntity relationshipPropertiesEntity, - Object relationshipProperties, Object relatedEntity - ) { + private PersistentPropertyAccessor relationshipPropertiesPropertyAccessor; + + private Object relationshipProperties; + + RelationshipPropertiesWithEntityHolder(Neo4jPersistentEntity relationshipPropertiesEntity, + Object relationshipProperties, Object relatedEntity) { this.relationshipPropertiesEntity = relationshipPropertiesEntity; - this.relationshipPropertiesPropertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(relationshipProperties); + this.relationshipPropertiesPropertyAccessor = relationshipPropertiesEntity + .getPropertyAccessor(relationshipProperties); this.relationshipProperties = relationshipProperties; this.relatedEntity = relatedEntity; } public PersistentPropertyAccessor getRelationshipPropertiesPropertyAccessor() { - return relationshipPropertiesPropertyAccessor; + return this.relationshipPropertiesPropertyAccessor; } public Object getRelationshipProperties() { - return relationshipProperties; + return this.relationshipProperties; } private void setRelationshipProperties(Object relationshipProperties) { this.relationshipProperties = relationshipProperties; - this.relationshipPropertiesPropertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(this.relationshipProperties); + this.relationshipPropertiesPropertyAccessor = this.relationshipPropertiesEntity + .getPropertyAccessor(this.relationshipProperties); } public Object getRelatedEntity() { - return relatedEntity; + return this.relatedEntity; } @Override @@ -235,19 +250,21 @@ public final class MappingSupport { return false; } RelationshipPropertiesWithEntityHolder that = (RelationshipPropertiesWithEntityHolder) o; - return relationshipProperties.equals(that.relationshipProperties) && relatedEntity.equals(that.relatedEntity); + return this.relationshipProperties.equals(that.relationshipProperties) + && this.relatedEntity.equals(that.relatedEntity); } @Override public int hashCode() { - return Objects.hash(relationshipProperties, relatedEntity); + return Objects.hash(this.relationshipProperties, this.relatedEntity); } @Override public String toString() { - return "RelationshipPropertiesWithEntityHolder{" + - "relationshipProperties=" + relationshipProperties + - '}'; + return "RelationshipPropertiesWithEntityHolder{" + "relationshipProperties=" + this.relationshipProperties + + '}'; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jEntityConverter.java index aba5a170e..4d19b1696 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jEntityConverter.java @@ -19,6 +19,7 @@ import java.util.Map; import org.apiguardian.api.API; import org.neo4j.driver.types.MapAccessor; + import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityWriter; @@ -26,9 +27,10 @@ import org.springframework.data.convert.EntityWriter; * This orchestrates the built-in store conversions and any additional Spring converters. * * @author Michael J. Simons - * @soundtrack The Kleptones - A Night At The Hip-Hopera * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") -public interface Neo4jEntityConverter extends EntityReader, EntityWriter> { +public interface Neo4jEntityConverter + extends EntityReader, EntityWriter> { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index a2afbc946..9aaa46486 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -41,6 +41,7 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.types.TypeSystem; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -73,8 +74,8 @@ import org.springframework.util.ReflectionUtils; /** * An implementation of both a {@link Schema} as well as a Neo4j version of Spring Data's - * {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to provide the initial set of - * classes through {@link #setInitialEntitySet(Set)}. + * {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to + * provide the initial set of classes through {@link #setInitialEntitySet(Set)}. * * @author Michael J. Simons * @author Gerrit Meier @@ -85,24 +86,26 @@ public final class Neo4jMappingContext extends AbstractMappingContext> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class)); /** - * A map of fallback id generators, that have not been added to the application context + * A map of fallback id generators, that have not been added to the application + * context. */ private final Map>, IdGenerator> idGenerators = new ConcurrentHashMap<>(); private final Map, Neo4jPersistentPropertyConverterFactory> converterFactories = new ConcurrentHashMap<>(); /** - * The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break the dependency cycle between - * this class and the {@link DefaultNeo4jEntityConverter}. + * The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break + * the dependency cycle between this class and the + * {@link DefaultNeo4jEntityConverter}. */ private final NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore(); @@ -112,6 +115,8 @@ public final class Neo4jMappingContext extends AbstractMappingContext, Set> postLoadMethods = new ConcurrentHashMap<>(); + private final Lazy propertyCharacteristicsProvider; + private EventSupport eventSupport; @Nullable @@ -119,58 +124,6 @@ public final class Neo4jMappingContext extends AbstractMappingContext propertyCharacteristicsProvider; - - /** - * A builder for creating custom instances of a {@link Neo4jMappingContext}. - * @since 6.3.7 - */ - public static class Builder { - - private Neo4jConversions neo4jConversions; - - private TypeSystem typeSystem; - - @Nullable - private PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider; - - private Builder() { - this(new Neo4jConversions(), null, null); - } - - private Builder(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem, @Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) { - this.neo4jConversions = neo4jConversions; - this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault); - this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider; - } - - @SuppressWarnings("HiddenField") - public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) { - this.neo4jConversions = neo4jConversions; - return this; - } - - @SuppressWarnings("HiddenField") - public Builder withPersistentPropertyCharacteristicsProvider(PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) { - this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider; - return this; - } - - @SuppressWarnings("HiddenField") - public Builder withTypeSystem(TypeSystem typeSystem) { - this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault); - return this; - } - - public Neo4jMappingContext build() { - return new Neo4jMappingContext(this); - } - } - - public static Builder builder() { - return new Builder(); - } - public Neo4jMappingContext() { this(new Builder()); @@ -184,21 +137,61 @@ public final class Neo4jMappingContext extends AbstractMappingContext characteristicsProvider != null || this.beanFactory == null ? - characteristicsProvider : this.beanFactory.getBeanProvider(PersistentPropertyCharacteristicsProvider.class).getIfUnique()); + this.propertyCharacteristicsProvider = Lazy + .of(() -> (characteristicsProvider != null || this.beanFactory == null) ? characteristicsProvider + : this.beanFactory.getBeanProvider(PersistentPropertyCharacteristicsProvider.class).getIfUnique()); + } + + public static Builder builder() { + return new Builder(); + } + + private static boolean isValidParentNode(Class parentClass) { + if (parentClass == null || parentClass.equals(Object.class)) { + return false; + } + + // Either a concrete class explicitly annotated as Node or an abstract class + return Modifier.isAbstract(parentClass.getModifiers()) || parentClass.isAnnotationPresent(Node.class); + } + + private static boolean isValidEntityInterface(Class typeInterface) { + return typeInterface.isAnnotationPresent(Node.class); + } + + private static Set computePostLoadMethods(Neo4jPersistentEntity entity) { + + Set postLoadMethods = new LinkedHashSet<>(); + ReflectionUtils.MethodFilter isValidPostLoad = method -> { + int modifiers = method.getModifiers(); + return !Modifier.isStatic(modifiers) && method.getParameterCount() == 0 + && VOID_TYPES.contains(method.getReturnType()) + && AnnotationUtils.findAnnotation(method, PostLoad.class) != null; + }; + Class underlyingClass = entity.getUnderlyingClass(); + ReflectionUtils.doWithMethods(underlyingClass, method -> postLoadMethods.add(new MethodHolder(method, null)), + isValidPostLoad); + if (KotlinDetector.isKotlinType(underlyingClass)) { + ReflectionUtils.doWithFields(underlyingClass, field -> { + ReflectionUtils.doWithMethods(field.getType(), + method -> postLoadMethods.add(new MethodHolder(method, field)), isValidPostLoad); + }, field -> field.isSynthetic() && field.getName().startsWith("$$delegate_")); + } + + return Collections.unmodifiableSet(postLoadMethods); } /** - * We need to set the context to non-strict in case we must dynamically add parent classes. As there is no - * way to access the original value without reflection, we track its change. - * - * @param strict The new value for the strict setting. + * We need to set the context to non-strict in case we must dynamically add parent + * classes. As there is no way to access the original value without reflection, we + * track its change. + * @param strict the new value for the strict setting */ @Override public void setStrict(boolean strict) { @@ -206,14 +199,14 @@ public final class Neo4jMappingContext extends AbstractMappingContext entity) { @@ -221,13 +214,9 @@ public final class Neo4jMappingContext extends AbstractMappingContext targetType) { - return conversionService.hasCustomWriteTarget(targetType); + return this.conversionService.hasCustomWriteTarget(targetType); } - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) - */ @Override protected Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { @@ -235,16 +224,19 @@ public final class Neo4jMappingContext extends AbstractMappingContext existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get( - primaryLabel); - if (existingEntity != null && !existingEntity.getTypeInformation().getRawTypeInformation() - .equals(typeInformation.getRawTypeInformation())) { + Neo4jPersistentEntity existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore + .get(primaryLabel); + if (existingEntity != null && !existingEntity.getTypeInformation() + .getRawTypeInformation() + .equals(typeInformation.getRawTypeInformation())) { String message = String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", primaryLabel); throw new MappingException(message); @@ -252,8 +244,11 @@ public final class Neo4jMappingContext extends AbstractMappingContext label = this.nodeDescriptionStore.entrySet().stream() - .filter(e -> e.getValue().equals(newEntity)).map(Map.Entry::getKey).findFirst(); + Optional label = this.nodeDescriptionStore.entrySet() + .stream() + .filter(e -> e.getValue().equals(newEntity)) + .map(Map.Entry::getKey) + .findFirst(); String message = String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", newEntity, @@ -287,7 +282,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext parentClass) { - if (parentClass == null || parentClass.equals(Object.class)) { - return false; - } - - // Either a concrete class explicitly annotated as Node or an abstract class - return Modifier.isAbstract(parentClass.getModifiers()) || - parentClass.isAnnotationPresent(Node.class); - } - - private static boolean isValidEntityInterface(Class typeInterface) { - return typeInterface.isAnnotationPresent(Node.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) - */ @Override protected Neo4jPersistentProperty createPersistentProperty(Property property, Neo4jPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { - PersistentPropertyCharacteristics optionalCharacteristics = this.propertyCharacteristicsProvider - .getOptional() - .flatMap(provider -> Optional.ofNullable(provider.apply(property, owner))) - .orElse(null); + PersistentPropertyCharacteristics optionalCharacteristics = this.propertyCharacteristicsProvider.getOptional() + .flatMap(provider -> Optional.ofNullable(provider.apply(property, owner))) + .orElse(null); return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder, optionalCharacteristics); } @Override - @Nullable - public NodeDescription getNodeDescription(String primaryLabel) { + @Nullable public NodeDescription getNodeDescription(String primaryLabel) { return this.nodeDescriptionStore.get(primaryLabel); } @Override - @Nullable - public NodeDescription getNodeDescription(Class underlyingClass) { + @Nullable public NodeDescription getNodeDescription(Class underlyingClass) { return doGetPersistentEntity(underlyingClass); } @Override - @Nullable - public Neo4jPersistentEntity getPersistentEntity(TypeInformation typeInformation) { + @Nullable public Neo4jPersistentEntity getPersistentEntity(TypeInformation typeInformation) { Neo4jPersistentEntity existingDescription = this.doGetPersistentEntity(typeInformation); if (existingDescription != null) { @@ -368,27 +341,20 @@ public final class Neo4jMappingContext extends AbstractMappingContext doGetPersistentEntity(TypeInformation typeInformation) { + @Nullable private Neo4jPersistentEntity doGetPersistentEntity(TypeInformation typeInformation) { return doGetPersistentEntity(typeInformation.getRawTypeInformation().getType()); } /** - * This checks whether a type is an interface and if so, tries to figure whether a persistent entity exists - * matching the name that can be derived from the interface. If the interface is assignable by the class by behind - * the retrieved entity, that entity will be used. Otherwise we will look for an entity matching the interface type - * itself. - * - * @param underlyingClass The underlying class - * @return An optional persistent entity + * This checks whether a type is an interface and if so, tries to figure whether a + * persistent entity exists matching the name that can be derived from the interface. + * If the interface is assignable by the class by behind the retrieved entity, that + * entity will be used. Otherwise we will look for an entity matching the interface + * type itself. + * @param underlyingClass the underlying class + * @return an optional persistent entity */ - @Nullable - private Neo4jPersistentEntity doGetPersistentEntity(Class underlyingClass) { + @Nullable private Neo4jPersistentEntity doGetPersistentEntity(Class underlyingClass) { if (underlyingClass.isInterface()) { String primaryLabel = DefaultNeo4jPersistentEntity.computePrimaryLabel(underlyingClass); @@ -405,9 +371,11 @@ public final class Neo4jMappingContext extends AbstractMappingContext { - // The beanFactory can't actually be reassigned, so doing a whole double lock check is a bit overkill + // The beanFactory can't actually be reassigned, so doing a whole double + // lock check is a bit overkill @SuppressWarnings("NullAway") var result = this.beanFactory.createBean(t); return result; @@ -431,16 +399,17 @@ public final class Neo4jMappingContext extends AbstractMappingContext findConstructor(Class clazz, Class... parameterTypes) { + @Nullable Constructor findConstructor(Class clazz, Class... parameterTypes) { try { return ReflectionUtils.accessibleConstructor(clazz, parameterTypes); - } catch (NoSuchMethodException e) { + } + catch (NoSuchMethodException ex) { return null; } } @@ -452,14 +421,14 @@ public final class Neo4jMappingContext extends AbstractMappingContext optionalConstructor; optionalConstructor = findConstructor(t, BeanFactory.class, Neo4jConversionService.class); if (optionalConstructor != null) { - return t.cast( - BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService)); + return t + .cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService)); } optionalConstructor = findConstructor(t, Neo4jConversionService.class, BeanFactory.class); if (optionalConstructor != null) { - return t.cast( - BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService)); + return t + .cast(BeanUtils.instantiateClass(optionalConstructor, this.beanFactory, this.conversionService)); } optionalConstructor = findConstructor(t, BeanFactory.class); @@ -475,12 +444,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext getOptionalCustomConversionsFor(Neo4jPersistentProperty persistentProperty) { + @Nullable Neo4jPersistentPropertyConverter getOptionalCustomConversionsFor(Neo4jPersistentProperty persistentProperty) { // Is the annotation present at all? if (!persistentProperty.isAnnotationPresent(ConvertWith.class)) { @@ -488,10 +452,10 @@ public final class Neo4jMappingContext extends AbstractMappingContext customConverter = persistentPropertyConverterFactory.getPropertyConverterFor( - persistentProperty); + Neo4jPersistentPropertyConverterFactory persistentPropertyConverterFactory = this + .getOrCreateConverterFactoryOfType(convertWith.converterFactory()); + Neo4jPersistentPropertyConverter customConverter = persistentPropertyConverterFactory + .getPropertyConverterFor(persistentProperty); boolean forCollection = false; if (persistentProperty.isCollectionLike()) { @@ -500,21 +464,25 @@ public final class Neo4jMappingContext extends AbstractMappingContext) ReflectionUtils.invokeMethod(getClassOfDelegate, customConverter); - } else { + } + else { converterClass = customConverter.getClass(); } - Map typeVariableMap = (converterClass != null) ? GenericTypeResolver.getTypeVariableMap(converterClass) - .entrySet() - .stream() - .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)) : Map.of(); + Map typeVariableMap = (converterClass != null) + ? GenericTypeResolver.getTypeVariableMap(converterClass) + .entrySet() + .stream() + .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)) + : Map.of(); Type propertyType = null; if (typeVariableMap.containsKey("T")) { propertyType = typeVariableMap.get("T"); - } else if (typeVariableMap.containsKey("P")) { + } + else if (typeVariableMap.containsKey("P")) { propertyType = typeVariableMap.get("P"); } - forCollection = propertyType instanceof ParameterizedType && - persistentProperty.getType().equals(((ParameterizedType) propertyType).getRawType()); + forCollection = propertyType instanceof ParameterizedType + && persistentProperty.getType().equals(((ParameterizedType) propertyType).getRawType()); } return new NullSafeNeo4jPersistentPropertyConverter<>(customConverter, persistentProperty.isComposite(), @@ -529,74 +497,69 @@ public final class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentEntity, - RelationshipDescription relationshipDescription, - List plainRelationshipRows, boolean canUseElementId) { + public CreateRelationshipStatementHolder createStatementForImperativeSimpleRelationshipBatch( + Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipDescription, + List plainRelationshipRows, boolean canUseElementId) { - return createStatementForSingleRelationship(neo4jPersistentEntity, (DefaultRelationshipDescription) relationshipDescription, - plainRelationshipRows, canUseElementId); + return createStatementForSingleRelationship(neo4jPersistentEntity, + (DefaultRelationshipDescription) relationshipDescription, plainRelationshipRows, canUseElementId); } public CreateRelationshipStatementHolder createStatementForImperativeRelationshipsWithPropertiesBatch(boolean isNew, - Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationshipDescription, - Object relatedValues, - List> relationshipPropertiesRows, - boolean canUseElementId) { + Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipDescription, + Object relatedValues, List> relationshipPropertiesRows, boolean canUseElementId) { - List relationshipPropertyValues = ((Collection) relatedValues).stream() - .map(MappingSupport.RelationshipPropertiesWithEntityHolder.class::cast).collect(Collectors.toList()); + List relationshipPropertyValues = ((Collection) relatedValues) + .stream() + .map(MappingSupport.RelationshipPropertiesWithEntityHolder.class::cast) + .collect(Collectors.toList()); return createStatementForRelationshipWithPropertiesBatch(isNew, neo4jPersistentEntity, relationshipDescription, relationshipPropertyValues, relationshipPropertiesRows, canUseElementId); } - public CreateRelationshipStatementHolder createStatementForSingleRelationship(Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationshipContext, - Object relatedValue, - boolean isNewRelationship, - boolean canUseElementId) { + public CreateRelationshipStatementHolder createStatementForSingleRelationship( + Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipContext, + Object relatedValue, boolean isNewRelationship, boolean canUseElementId) { if (relationshipContext.hasRelationshipProperties()) { - MappingSupport.RelationshipPropertiesWithEntityHolder relatedValueEntityHolder = - (MappingSupport.RelationshipPropertiesWithEntityHolder) ( - // either this is a scalar entity holder value - // or a dynamic relationship with - // either a list of entity holders - // or a scalar value - relatedValue instanceof MappingSupport.RelationshipPropertiesWithEntityHolder - ? relatedValue - : ((Map.Entry) relatedValue).getValue() instanceof List - ? ((List) ((Map.Entry) relatedValue).getValue()).get(0) - : ((Map.Entry) relatedValue).getValue()); + MappingSupport.RelationshipPropertiesWithEntityHolder relatedValueEntityHolder = (MappingSupport.RelationshipPropertiesWithEntityHolder) ( + // either this is a scalar entity holder value + // or a dynamic relationship with + // either a list of entity holders + // or a scalar value + (relatedValue instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) ? relatedValue + : (((Map.Entry) relatedValue).getValue() instanceof List) + ? ((List) ((Map.Entry) relatedValue).getValue()).get(0) + : ((Map.Entry) relatedValue).getValue()); String dynamicRelationshipType = null; if (relationshipContext.isDynamic()) { Neo4jPersistentProperty inverse = ((DefaultRelationshipDescription) relationshipContext).getInverse(); - TypeInformation keyType = inverse.getTypeInformation() - .getRequiredComponentType(); + TypeInformation keyType = inverse.getTypeInformation().getRequiredComponentType(); Object key = ((Map.Entry) relatedValue).getKey(); - dynamicRelationshipType = conversionService.writeValue(key, keyType, - inverse.getOptionalConverter()).asString(); + dynamicRelationshipType = this.conversionService + .writeValue(key, keyType, inverse.getOptionalConverter()) + .asString(); } - return createStatementForRelationshipWithProperties( - neo4jPersistentEntity, relationshipContext, - dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship, canUseElementId - ); - } else { - return createStatementForSingleRelationship(neo4jPersistentEntity, (DefaultRelationshipDescription) relationshipContext, - relatedValue, canUseElementId); + return createStatementForRelationshipWithProperties(neo4jPersistentEntity, relationshipContext, + dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship, canUseElementId); + } + else { + return createStatementForSingleRelationship(neo4jPersistentEntity, + (DefaultRelationshipDescription) relationshipContext, relatedValue, canUseElementId); } } private CreateRelationshipStatementHolder createStatementForRelationshipWithProperties( - Neo4jPersistentEntity neo4jPersistentEntity, - RelationshipDescription relationshipDescription, @Nullable String dynamicRelationshipType, - MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship, boolean canUseElementId) { + Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipDescription, + @Nullable String dynamicRelationshipType, + MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship, + boolean canUseElementId) { Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties( - neo4jPersistentEntity, relationshipDescription, isNewRelationship, - dynamicRelationshipType, canUseElementId, false); + neo4jPersistentEntity, relationshipDescription, isNewRelationship, dynamicRelationshipType, + canUseElementId, false); Map propMap = new HashMap<>(); // write relationship properties @@ -605,16 +568,13 @@ public final class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentEntity, - RelationshipDescription relationshipDescription, + private CreateRelationshipStatementHolder createStatementForRelationshipWithPropertiesBatch(boolean isNew, + Neo4jPersistentEntity neo4jPersistentEntity, RelationshipDescription relationshipDescription, List relatedValues, - List> relationshipPropertiesRows, - boolean canUseElementId) { + List> relationshipPropertiesRows, boolean canUseElementId) { - Statement relationshipCreationQuery = CypherGenerator.INSTANCE - .prepareUpdateOfRelationshipsWithProperties(neo4jPersistentEntity, relationshipDescription, isNew, canUseElementId); + Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareUpdateOfRelationshipsWithProperties( + neo4jPersistentEntity, relationshipDescription, isNew, canUseElementId); List relationshipRows = new ArrayList<>(); Map relationshipPropertiesEntries = new HashMap<>(); if (isNew) { @@ -631,31 +591,32 @@ public final class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentEntity, - DefaultRelationshipDescription relationshipDescription, Object relatedValue, boolean canUseElementId) { + Neo4jPersistentEntity neo4jPersistentEntity, DefaultRelationshipDescription relationshipDescription, + Object relatedValue, boolean canUseElementId) { String relationshipType; if (!relationshipDescription.isDynamic()) { relationshipType = null; - } else { + } + else { Neo4jPersistentProperty inverse = relationshipDescription.getInverse(); TypeInformation keyType = inverse.getTypeInformation().getRequiredComponentType(); Object key = ((Map.Entry) relatedValue).getKey(); - relationshipType = conversionService.writeValue(key, keyType, inverse.getOptionalConverter()).asString(); + relationshipType = this.conversionService.writeValue(key, keyType, inverse.getOptionalConverter()) + .asString(); } - Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationships( - neo4jPersistentEntity, relationshipDescription, relationshipType, canUseElementId); + Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationships(neo4jPersistentEntity, + relationshipDescription, relationshipType, canUseElementId); return new CreateRelationshipStatementHolder(relationshipCreationQuery, Collections.emptyMap()); } /** * Executes all post load methods of the given instance. - * - * @param entity The entity definition - * @param instance The instance whose post load methods should be executed - * @param Type of the entity - * @return The instance + * @param entity the entity definition + * @param instance the instance whose post load methods should be executed + * @param type of the entity + * @return the instance */ public T invokePostLoad(Neo4jPersistentEntity entity, T instance) { @@ -667,25 +628,54 @@ public final class Neo4jMappingContext extends AbstractMappingContext computePostLoadMethods(Neo4jPersistentEntity entity) { + /** + * A builder for creating custom instances of a {@link Neo4jMappingContext}. + * + * @since 6.3.7 + */ + public static final class Builder { - Set postLoadMethods = new LinkedHashSet<>(); - ReflectionUtils.MethodFilter isValidPostLoad = method -> { - int modifiers = method.getModifiers(); - return !Modifier.isStatic(modifiers) && method.getParameterCount() == 0 && VOID_TYPES.contains( - method.getReturnType()) && AnnotationUtils.findAnnotation(method, PostLoad.class) != null; - }; - Class underlyingClass = entity.getUnderlyingClass(); - ReflectionUtils.doWithMethods(underlyingClass, method -> postLoadMethods.add(new MethodHolder(method, null)), - isValidPostLoad); - if (KotlinDetector.isKotlinType(underlyingClass)) { - ReflectionUtils.doWithFields(underlyingClass, field -> { - ReflectionUtils.doWithMethods(field.getType(), - method -> postLoadMethods.add(new MethodHolder(method, field)), isValidPostLoad); - }, field -> field.isSynthetic() && field.getName().startsWith("$$delegate_")); + private Neo4jConversions neo4jConversions; + + private TypeSystem typeSystem; + + @Nullable + private PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider; + + private Builder() { + this(new Neo4jConversions(), null, null); + } + + private Builder(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem, + @Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) { + this.neo4jConversions = neo4jConversions; + this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault); + this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider; + } + + @SuppressWarnings("HiddenField") + public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) { + this.neo4jConversions = neo4jConversions; + return this; + } + + @SuppressWarnings("HiddenField") + public Builder withPersistentPropertyCharacteristicsProvider( + PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) { + this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider; + return this; + } + + @SuppressWarnings("HiddenField") + public Builder withTypeSystem(TypeSystem typeSystem) { + this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault); + return this; + } + + public Neo4jMappingContext build() { + return new Neo4jMappingContext(this); } - return Collections.unmodifiableSet(postLoadMethods); } static class MethodHolder { @@ -700,27 +690,31 @@ public final class Neo4jMappingContext extends AbstractMappingContext - * Note to the outside world, we treat the {@link org.springframework.data.neo4j.core.schema.TargetNode @TargetNode} - * annotated field of a {@link org.springframework.data.neo4j.core.schema.RelationshipProperties @RelationshipProperties} annotated - * class as association. Internally, we treat it as a property + * Note to the outside world, we treat the + * {@link org.springframework.data.neo4j.core.schema.TargetNode @TargetNode} annotated + * field of a + * {@link org.springframework.data.neo4j.core.schema.RelationshipProperties @RelationshipProperties} + * annotated class as association. Internally, we treat it as a property * - * @author Michael J. Simons * @param type of the underlying class + * @author Michael J. Simons * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface Neo4jPersistentEntity extends MutablePersistentEntity, NodeDescription { + /** + * Types that are bound to Neo4j id() and are officially deprecated from Neo4j. + */ Set> DEPRECATED_GENERATED_ID_TYPES = Set.of(Long.class, long.class); /** - * @return An optional property pointing to a {@link java.util.Collection Collection<String>} containing dynamic - * "runtime managed" labels. + * A collection liked property containing labels to be stored dynamically with this + * entity. + * @return an optional property pointing to a {@link java.util.Collection + * Collection<String>} containing dynamic "runtime managed" labels. */ Optional getDynamicLabelsProperty(); /** - * Determines if the entity is annotated with {@link org.springframework.data.neo4j.core.schema.RelationshipProperties} - * + * Determines if the entity is annotated with + * {@link org.springframework.data.neo4j.core.schema.RelationshipProperties}. * @return true if this is a relationship properties class, otherwise false. */ boolean isRelationshipPropertiesEntity(); /** - * Determines if the entity is annotated with {@link org.springframework.data.neo4j.core.schema.RelationshipProperties} - * and has the flag {@link org.springframework.data.neo4j.core.schema.RelationshipProperties#persistTypeInfo()} set to true. - * @return true if this is a relationship properties class and the type info should be persisted, otherwise false. + * Determines if the entity is annotated with + * {@link org.springframework.data.neo4j.core.schema.RelationshipProperties} and has + * the flag + * {@link org.springframework.data.neo4j.core.schema.RelationshipProperties#persistTypeInfo()} + * set to true. + * @return true if this is a relationship properties class and the type info should be + * persisted, otherwise false. */ boolean hasRelationshipPropertyPersistTypeInfoFlag(); /** - * @return True if the underlying domain classes uses {@code id()} to compute internally generated ids. + * Checks if this entity is using deprecated internal ids anywhere in its hierarchy. + * @return true if the underlying domain classes uses {@code id()} to compute + * internally generated ids. */ default boolean isUsingDeprecatedInternalId() { for (NodeDescription nodeDescription : getChildNodeDescriptionsInHierarchy()) { - if (nodeDescription.isUsingInternalIds() && ((Neo4jPersistentEntity) nodeDescription).getIdProperty() != null - && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(((Neo4jPersistentEntity) nodeDescription).getIdProperty().getType())) { + if (nodeDescription.isUsingInternalIds() + && ((Neo4jPersistentEntity) nodeDescription).getIdProperty() != null + && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES + .contains(((Neo4jPersistentEntity) nodeDescription).getIdProperty().getType())) { return true; } } - return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType()); + return isUsingInternalIds() + && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType()); } /** + * Returns true if this entity spots a vector property. * @return true if this entity spots a vector property */ boolean hasVectorProperty(); /** - * Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise {@literal null}. + * Will return the single supported vector property if {@link #hasVectorProperty()} + * returns {@literal true}, otherwise {@literal null}. * @return an optional vector property on this entity */ - @Nullable - Neo4jPersistentProperty getVectorProperty(); + @Nullable Neo4jPersistentProperty getVectorProperty(); /** - * Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise it will throw an {@link IllegalStateException}. + * Will return the single supported vector property if {@link #hasVectorProperty()} + * returns {@literal true}, otherwise it will throw an {@link IllegalStateException}. * @return the vector property on this entity. */ Neo4jPersistentProperty getRequiredVectorProperty(); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index 0b9fff21f..0aeff87d9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Vector; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; @@ -26,8 +27,8 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty; import org.springframework.data.neo4j.core.schema.DynamicLabels; /** - * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related - * to Neo4j. + * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional + * methods for metadata related to Neo4j. * * @author Michael J. Simons * @author Philipp Tölle @@ -38,22 +39,23 @@ import org.springframework.data.neo4j.core.schema.DynamicLabels; public interface Neo4jPersistentProperty extends PersistentProperty, GraphPropertyDescription { /** - * Dynamic associations are associations to non-simple types stored in a map with a key type of - * {@literal java.lang.String} or enum. - * - * @return True, if this association is a dynamic association. + * Dynamic associations are associations to non-simple types stored in a map with a + * key type of {@literal java.lang.String} or enum. + * @return true, if this association is a dynamic association. */ default boolean isDynamicAssociation() { Class componentType = getComponentType(); - return isRelationship() && isMap() && (componentType == String.class || (componentType != null && componentType.isEnum())); + return isRelationship() && isMap() + && (componentType == String.class || (componentType != null && componentType.isEnum())); } /** - * Dynamic one-to-many associations are associations to non-simple types stored in a map with a key type of - * {@literal java.lang.String} and values of {@literal java.util.Collection}. - * - * @return True, if this association is a dynamic association with multiple values per type. + * Dynamic one-to-many associations are associations to non-simple types stored in a + * map with a key type of {@literal java.lang.String} and values of + * {@literal java.util.Collection}. + * @return true, if this association is a dynamic association with multiple values per + * type. * @since 6.0.1 */ default boolean isDynamicOneToManyAssociation() { @@ -62,6 +64,8 @@ public interface Neo4jPersistentProperty extends PersistentProperty getOptionalConverter(); + @Nullable Neo4jPersistentPropertyConverter getOptionalConverter(); /** - * @return True if this property targets an entity which is a container for relationship properties. + * Returns true if this property belongs to a relationship. + * @return true if this property targets an entity which is a container for + * relationship properties. */ boolean isEntityWithRelationshipProperties(); /** - * Computes a prefix to be used on multiple properties on a node when this persistent property is annotated with - * {@link CompositeProperty @CompositeProperty}. - * - * @return A valid prefix + * Computes a prefix to be used on multiple properties on a node when this persistent + * property is annotated with {@link CompositeProperty @CompositeProperty}. + * @return a valid prefix */ default String computePrefixWithDelimiter() { CompositeProperty compositeProperty = getRequiredAnnotation(CompositeProperty.class); - return Optional.of(compositeProperty.prefix()).map(String::trim).filter(s -> !s.isEmpty()) - .orElseGet(this::getFieldName) + compositeProperty.delimiter(); + return Optional.of(compositeProperty.prefix()) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .orElseGet(this::getFieldName) + compositeProperty.delimiter(); } /** + * A flag is this is a read only property, can be changed via + * {@link PersistentPropertyCharacteristics}. * @return {@literal true} if this is a read only property. */ default boolean isReadOnly() { return false; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java index f8236af3d..3fb974ce5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java @@ -25,6 +25,7 @@ import java.util.Objects; import org.apiguardian.api.API; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; @@ -32,9 +33,10 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * Working on nested relationships happens in a certain algorithmic context. This context enables a tight cohesion - * between the algorithmic steps and the data, these steps are performed on. In our the interaction happens between the - * data that describes the relationship and the specific steps of the algorithm. + * Working on nested relationships happens in a certain algorithmic context. This context + * enables a tight cohesion between the algorithmic steps and the data, these steps are + * performed on. In our the interaction happens between the data that describes the + * relationship and the specific steps of the algorithm. * * @author Philipp Tölle * @author Gerrit Meier @@ -43,9 +45,12 @@ import org.springframework.data.neo4j.core.schema.TargetNode; */ @API(status = API.Status.INTERNAL, since = "6.0") public final class NestedRelationshipContext { + private final Neo4jPersistentProperty inverse; + @Nullable private final Object value; + private final RelationshipDescription relationship; private final boolean inverseValueIsEmpty; @@ -58,84 +63,31 @@ public final class NestedRelationshipContext { this.inverseValueIsEmpty = inverseValueIsEmpty; } - public boolean isReadOnly() { - return inverse.isAnnotationPresent(ReadOnlyProperty.class); - } - - public Neo4jPersistentProperty getInverse() { - return inverse; - } - - @Nullable - public Object getValue() { - return value; - } - - public RelationshipDescription getRelationship() { - return relationship; - } - - public boolean inverseValueIsEmpty() { - return inverseValueIsEmpty; - } - - boolean hasRelationshipWithProperties() { - return this.relationship.hasRelationshipProperties(); - } - - public Object identifyAndExtractRelationshipTargetNode(Object relatedValue) { - Object valueToBeSaved = relatedValue; - if (relatedValue instanceof Map.Entry relatedValueMapEntry) { - if (this.hasRelationshipWithProperties()) { - Object mapValue = relatedValueMapEntry.getValue(); - // it can be either a scalar entity holder or a list of it - mapValue = mapValue instanceof List ? ((List) mapValue).get(0) : mapValue; - valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue).getRelatedEntity(); - } else if (this.getInverse().isDynamicAssociation()) { - valueToBeSaved = relatedValueMapEntry.getValue(); - } - } else if (this.hasRelationshipWithProperties()) { - // here comes the entity - valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue).getRelatedEntity(); - } - - return valueToBeSaved; - } - - @Nullable - public PersistentPropertyAccessor getRelationshipPropertiesPropertyAccessor(Object relatedValue) { - - if (!this.hasRelationshipWithProperties() || relatedValue == null) { - return null; - } - - if (relatedValue instanceof Map.Entry) { - Object mapValue = ((Map.Entry) relatedValue).getValue(); - mapValue = mapValue instanceof List ? ((List) mapValue).get(0) : mapValue; - return ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue).getRelationshipPropertiesPropertyAccessor(); - } else { - return ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue).getRelationshipPropertiesPropertyAccessor(); - } - } - public static NestedRelationshipContext of(Association<@NonNull Neo4jPersistentProperty> handler, PersistentPropertyAccessor propertyAccessor, Neo4jPersistentEntity neo4jPersistentEntity) { Neo4jPersistentProperty inverse = handler.getInverse(); - // value can be a collection or scalar of related notes, point to a relationship property (scalar or collection) + // value can be a collection or scalar of related notes, point to a relationship + // property (scalar or collection) // or is a dynamic relationship (map) Object value = propertyAccessor.getProperty(inverse); boolean inverseValueIsEmpty = value == null; - RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((PropertyFilter.NO_FILTER)).stream() - .filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().orElseThrow(() -> new MappingException( - neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName())); + RelationshipDescription relationship = neo4jPersistentEntity + .getRelationshipsInHierarchy((PropertyFilter.NO_FILTER)) + .stream() + .filter(r -> r.getFieldName().equals(inverse.getName())) + .findFirst() + .orElseThrow(() -> new MappingException( + neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName())); if (relationship.hasRelationshipProperties() && value != null) { - Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship.getRequiredRelationshipPropertiesEntity(); + Neo4jPersistentEntity relationshipPropertiesEntity = (Neo4jPersistentEntity) relationship + .getRequiredRelationshipPropertiesEntity(); - // If this is dynamic relationship (Map), extract the keys as relationship names + // If this is dynamic relationship (Map), extract the keys as + // relationship names // and the map values as values. // The values themselves can be either a scalar or a List. if (relationship.isDynamic()) { @@ -148,38 +100,37 @@ public final class NestedRelationshipContext { if (mapEntryValue instanceof List) { for (Object relationshipProperty : ((List) mapEntryValue)) { - MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = - new MappingSupport.RelationshipPropertiesWithEntityHolder( - relationshipPropertiesEntity, relationshipProperty, - getTargetNode(relationshipPropertiesEntity, relationshipProperty)); + MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = new MappingSupport.RelationshipPropertiesWithEntityHolder( + relationshipPropertiesEntity, relationshipProperty, + getTargetNode(relationshipPropertiesEntity, relationshipProperty)); relationshipValues.add(oneOfThem); } - } else { // scalar - MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = - new MappingSupport.RelationshipPropertiesWithEntityHolder( - relationshipPropertiesEntity, mapEntryValue, - getTargetNode(relationshipPropertiesEntity, mapEntryValue)); + } + else { // scalar + MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = new MappingSupport.RelationshipPropertiesWithEntityHolder( + relationshipPropertiesEntity, mapEntryValue, + getTargetNode(relationshipPropertiesEntity, mapEntryValue)); relationshipProperties.put(mapEntry.getKey(), oneOfThem); } } value = relationshipProperties; - } else { + } + else { if (inverse.isCollectionLike()) { List relationshipProperties = new ArrayList<>(); for (Object relationshipProperty : ((Collection) value)) { - MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = - new MappingSupport.RelationshipPropertiesWithEntityHolder( - relationshipPropertiesEntity, relationshipProperty, - getTargetNode(relationshipPropertiesEntity, relationshipProperty)); + MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem = new MappingSupport.RelationshipPropertiesWithEntityHolder( + relationshipPropertiesEntity, relationshipProperty, + getTargetNode(relationshipPropertiesEntity, relationshipProperty)); relationshipProperties.add(oneOfThem); } value = relationshipProperties; - } else { + } + else { value = new MappingSupport.RelationshipPropertiesWithEntityHolder(relationshipPropertiesEntity, - value, - getTargetNode(relationshipPropertiesEntity, value)); + value, getTargetNode(relationshipPropertiesEntity, value)); } } } @@ -190,8 +141,74 @@ public final class NestedRelationshipContext { private static Object getTargetNode(Neo4jPersistentEntity relationshipPropertiesEntity, Object object) { PersistentPropertyAccessor propertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(object); - var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); + var targetNodeProperty = Objects.requireNonNull( + relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), + () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())); return Objects.requireNonNull(propertyAccessor.getProperty(targetNodeProperty)); } + + public boolean isReadOnly() { + return this.inverse.isAnnotationPresent(ReadOnlyProperty.class); + } + + public Neo4jPersistentProperty getInverse() { + return this.inverse; + } + + @Nullable public Object getValue() { + return this.value; + } + + public RelationshipDescription getRelationship() { + return this.relationship; + } + + public boolean inverseValueIsEmpty() { + return this.inverseValueIsEmpty; + } + + boolean hasRelationshipWithProperties() { + return this.relationship.hasRelationshipProperties(); + } + + public Object identifyAndExtractRelationshipTargetNode(Object relatedValue) { + Object valueToBeSaved = relatedValue; + if (relatedValue instanceof Map.Entry relatedValueMapEntry) { + if (this.hasRelationshipWithProperties()) { + Object mapValue = relatedValueMapEntry.getValue(); + // it can be either a scalar entity holder or a list of it + mapValue = (mapValue instanceof List) ? ((List) mapValue).get(0) : mapValue; + valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue).getRelatedEntity(); + } + else if (this.getInverse().isDynamicAssociation()) { + valueToBeSaved = relatedValueMapEntry.getValue(); + } + } + else if (this.hasRelationshipWithProperties()) { + // here comes the entity + valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue).getRelatedEntity(); + } + + return valueToBeSaved; + } + + @Nullable public PersistentPropertyAccessor getRelationshipPropertiesPropertyAccessor(Object relatedValue) { + + if (!this.hasRelationshipWithProperties() || relatedValue == null) { + return null; + } + + if (relatedValue instanceof Map.Entry) { + Object mapValue = ((Map.Entry) relatedValue).getValue(); + mapValue = (mapValue instanceof List) ? ((List) mapValue).get(0) : mapValue; + return ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue) + .getRelationshipPropertiesPropertyAccessor(); + } + else { + return ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue) + .getRelationshipPropertiesPropertyAccessor(); + } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java index 73427b6db..45965cfd8 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java @@ -26,29 +26,21 @@ import java.util.concurrent.locks.StampedLock; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.util.Assert; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.util.Assert; + /** - * This stores all processed nested relations and objects during save of objects so that the recursive descent can be - * stopped accordingly. + * This stores all processed nested relations and objects during save of objects so that + * the recursive descent can be stopped accordingly. * * @author Michael J. Simons - * @soundtrack Helge Schneider - Heart Attack No. 1 */ @API(status = API.Status.INTERNAL, since = "6.0") public final class NestedRelationshipProcessingStateMachine { - /** - * Valid processing states. - */ - public enum ProcessState { - PROCESSED_NONE, PROCESSED_BOTH, PROCESSED_ALL_RELATIONSHIPS, PROCESSED_ALL_VALUES - } - private final StampedLock lock = new StampedLock(); private final Neo4jMappingContext mappingContext; @@ -65,8 +57,8 @@ public final class NestedRelationshipProcessingStateMachine { private final Map processedObjectsAlias = new HashMap<>(); /** - * A map pointing from a processed object to the internal id. - * This will be useful during the persistence to avoid another DB network round-trip. + * A map pointing from a processed object to the internal id. This will be useful + * during the persistence to avoid another DB network round-trip. */ private final Map processedObjectsIds = new HashMap<>(); @@ -81,7 +73,8 @@ public final class NestedRelationshipProcessingStateMachine { this.mappingContext = mappingContext; } - public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext, @Nullable Object initialObject, @Nullable Object elementId) { + public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext, + @Nullable Object initialObject, @Nullable Object elementId) { this(mappingContext); if (initialObject != null && elementId != null) { @@ -97,15 +90,20 @@ public final class NestedRelationshipProcessingStateMachine { } /** - * @param relationshipDescription Check whether this relationship description has been processed - * @param valuesToStore Check whether all the values in the collection have been processed - * @return The state of things processed + * Retrieves the state for a given id. + * @param fromId the originating id to be checked. + * @param relationshipDescription check whether this relationship description has been + * processed + * @param valuesToStore check whether all the values in the collection have been + * processed + * @return the state of things processed */ - public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, Collection valuesToStore) { + public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, + Collection valuesToStore) { if (fromId == null) { return ProcessState.PROCESSED_BOTH; } - final long stamp = lock.readLock(); + final long stamp = this.lock.readLock(); try { boolean hasProcessedRelationship = hasProcessedRelationship(fromId, relationshipDescription); boolean hasProcessedAllValues = hasProcessedAllOf(valuesToStore); @@ -119,77 +117,48 @@ public final class NestedRelationshipProcessingStateMachine { return ProcessState.PROCESSED_ALL_VALUES; } return ProcessState.PROCESSED_NONE; - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } /** - * Combination of relationship description and fromId to differentiate between `equals`-wise equal relationship - * descriptions by their source identifier. This is needed because sometimes the very same relationship definition - * can get processed for different objects of the same entity. - * One could say that this is a Tuple but it has a nicer name. + * Marks the passed objects as processed. + * @param fromId the originating id to be checked. + * @param relationshipDescription to be marked as processed */ - private record RelationshipDescriptionWithSourceId(Object id, RelationshipDescription relationshipDescription) { - } - - private record ProcessedRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder entityHolder, - Object source, Object target, RelationshipDescription relationshipDescription) { - } - - private record RelationshipIdUpdateContext(Statement cypher, Object fromId, Object toId, - NestedRelationshipContext relationshipContext, - Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) { - } - - /** - * Supplier for arbitrary relationship ids - */ - @FunctionalInterface - public interface RelationshipIdSupplier { - Optional getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId); - } - - /** - * Reactive Supplier for arbitrary relationship ids - */ - @FunctionalInterface - public interface ReactiveRelationshipIdSupplier { - Mono getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId); - } - - /** - * Marks the passed objects as processed - * - * @param relationshipDescription To be marked as processed - */ - public void markRelationshipAsProcessed(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) { + public void markRelationshipAsProcessed(@Nullable Object fromId, + @Nullable RelationshipDescription relationshipDescription) { if (fromId == null || relationshipDescription == null) { return; } - final long stamp = lock.writeLock(); + final long stamp = this.lock.writeLock(); try { - this.processedRelationshipDescriptions.add(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); - } finally { - lock.unlock(stamp); + this.processedRelationshipDescriptions + .add(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); + } + finally { + this.lock.unlock(stamp); } } /** - * Marks the passed objects as processed - * - * @param valueToStore If not {@literal null}, all non-null values will be marked as processed - * @param elementId The internal id of the value processed + * Marks the passed objects as processed. + * @param valueToStore if not {@literal null}, all non-null values will be marked as + * processed + * @param elementId the internal id of the value processed */ public void markEntityAsProcessed(Object valueToStore, Object elementId) { - final long stamp = lock.writeLock(); + final long stamp = this.lock.writeLock(); try { doMarkValueAsProcessed(valueToStore, elementId); storeProcessedInAlias(valueToStore, valueToStore); - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } @@ -202,224 +171,313 @@ public final class NestedRelationshipProcessingStateMachine { /** * Checks if the value has already been processed. - * * @param value the object that should be looked for in the registry. * @return processed yes (true) / no (false) */ public boolean hasProcessedValue(Object value) { - long stamp = lock.readLock(); + long stamp = this.lock.readLock(); try { Object valueToCheck = extractRelatedValueFromRelationshipProperties(value); boolean processed = hasProcessed(valueToCheck); - // This can be the case the object has been loaded via an additional findXXX call + // This can be the case the object has been loaded via an additional findXXX + // call // We can enforce sets and so on, but this is more user-friendly Class typeOfValue = valueToCheck.getClass(); - if (!processed && mappingContext.hasPersistentEntityFor(typeOfValue)) { - Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(typeOfValue); + if (!processed && this.mappingContext.hasPersistentEntityFor(typeOfValue)) { + Neo4jPersistentEntity entity = this.mappingContext.getRequiredPersistentEntity(typeOfValue); Neo4jPersistentProperty idProperty = entity.getIdProperty(); Object id; Optional alreadyProcessedObject = Optional.empty(); if (idProperty != null) { - // After the lookup by system.identityHashCode failed for a processed object alias, - // we must traverse or iterate over all value with the matching type and compare the domain ids - // to figure out if the logical object has already been processed through a different object instance. - // The type check is needed to avoid relationship ids <> node id conflicts. + // After the lookup by system.identityHashCode failed for a processed + // object alias, + // we must traverse or iterate over all value with the matching type + // and compare the domain ids + // to figure out if the logical object has already been processed + // through a different object instance. + // The type check is needed to avoid relationship ids <> node id + // conflicts. id = entity.getPropertyAccessor(valueToCheck).getProperty(idProperty); - alreadyProcessedObject = processedObjectsAlias.values().stream() - .filter(typeOfValue::isInstance) - .filter(processedObject -> id != null && id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty))) - .findAny(); + alreadyProcessedObject = this.processedObjectsAlias.values() + .stream() + .filter(typeOfValue::isInstance) + .filter(processedObject -> id != null + && id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty))) + .findAny(); } - if (alreadyProcessedObject.isPresent()) { // Skip the show the next time around. + if (alreadyProcessedObject.isPresent()) { // Skip the show the next time + // around. processed = true; Object internalId = getObjectId(alreadyProcessedObject.get()); if (internalId != null) { - stamp = lock.tryConvertToWriteLock(stamp); + stamp = this.lock.tryConvertToWriteLock(stamp); doMarkValueAsProcessed(valueToCheck, internalId); } } } return processed; - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } /** * Checks if the relationship has already been processed. - * - * @param relationshipDescription the relationship that should be looked for in the registry. + * @param fromId the originating id to be checked. + * @param relationshipDescription the relationship that should be looked for in the + * registry. * @return processed yes (true) / no (false) */ - public boolean hasProcessedRelationship(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) { + public boolean hasProcessedRelationship(@Nullable Object fromId, + @Nullable RelationshipDescription relationshipDescription) { if (fromId == null || relationshipDescription == null) { return false; } - final long stamp = lock.readLock(); + final long stamp = this.lock.readLock(); try { - return processedRelationshipDescriptions.contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); - } finally { - lock.unlock(stamp); + return this.processedRelationshipDescriptions + .contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription)); + } + finally { + this.lock.unlock(stamp); } } - public void storeProcessRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder id, Object source, Object target, RelationshipDescription type) { - final long stamp = lock.writeLock(); + public void storeProcessRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder id, Object source, + Object target, RelationshipDescription type) { + final long stamp = this.lock.writeLock(); try { this.processedRelationshipEntities.add(new ProcessedRelationshipEntity(id, source, target, type)); - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } public boolean hasProcessedRelationshipEntity(Object source, Object target, RelationshipDescription type) { - final long stamp = lock.readLock(); + final long stamp = this.lock.readLock(); try { return this.processedRelationshipEntities.stream() - .anyMatch(r -> r.relationshipDescription().getType().equals(type.getType()) && r.relationshipDescription().getDirection().opposite() == type.getDirection() && ( - r.source() == source && r.target() == target || - r.target() == source && r.source() == target - )); - } finally { - lock.unlock(stamp); + .anyMatch(r -> r.relationshipDescription().getType().equals(type.getType()) + && r.relationshipDescription().getDirection().opposite() == type.getDirection() + && (r.source() == source && r.target() == target + || r.target() == source && r.source() == target)); + } + finally { + this.lock.unlock(stamp); } } - public void requireIdUpdate(Neo4jPersistentEntity sourceEntity, RelationshipDescription relationshipDescription, boolean canUseElementId, - Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) { + public void requireIdUpdate(Neo4jPersistentEntity sourceEntity, RelationshipDescription relationshipDescription, + boolean canUseElementId, Object fromId, Object toId, NestedRelationshipContext relationshipContext, + Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) { Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties( - sourceEntity, relationshipDescription, false, - null, canUseElementId, true); - final long stamp = lock.writeLock(); + sourceEntity, relationshipDescription, false, null, canUseElementId, true); + final long stamp = this.lock.writeLock(); try { - this.requiresIdUpdate.add(new RelationshipIdUpdateContext(relationshipCreationQuery, fromId, toId, relationshipContext, relatedValueToStore, idProperty)); - } finally { - lock.unlock(stamp); + this.requiresIdUpdate.add(new RelationshipIdUpdateContext(relationshipCreationQuery, fromId, toId, + relationshipContext, relatedValueToStore, idProperty)); + } + finally { + this.lock.unlock(stamp); } } public void updateRelationshipIds(RelationshipIdSupplier idSupplier) { - final long stamp = lock.writeLock(); + final long stamp = this.lock.writeLock(); try { - var it = requiresIdUpdate.iterator(); + var it = this.requiresIdUpdate.iterator(); while (it.hasNext()) { var requiredIdUpdate = it.next(); - idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()).ifPresent(anId -> { - PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext() + idSupplier + .getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), + requiredIdUpdate.toId()) + .ifPresent(anId -> { + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate + .relationshipContext() .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()); - if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { - relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), anId); - it.remove(); - } - }); + if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { + relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), anId); + it.remove(); + } + }); } - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } public Mono updateRelationshipIdsReactive(ReactiveRelationshipIdSupplier idSupplier) { return Flux.defer(() -> { - final long stamp = lock.writeLock(); - return Flux.fromIterable(requiresIdUpdate) - .flatMap(requiredIdUpdate -> Mono.just(requiredIdUpdate).zipWith(idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()))) - .doOnNext(t -> { - var requiredIdUpdate = t.getT1(); - PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext() - .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()); - if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { - relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), t.getT2()); - requiresIdUpdate.remove(requiredIdUpdate); - } - }).doOnTerminate(() -> lock.unlock(stamp)); + final long stamp = this.lock.writeLock(); + return Flux.fromIterable(this.requiresIdUpdate) + .flatMap(requiredIdUpdate -> Mono.just(requiredIdUpdate) + .zipWith(idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), + requiredIdUpdate.fromId(), requiredIdUpdate.toId()))) + .doOnNext(t -> { + var requiredIdUpdate = t.getT1(); + PersistentPropertyAccessor relationshipPropertiesPropertyAccessor = requiredIdUpdate + .relationshipContext() + .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()); + if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) { + relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), t.getT2()); + this.requiresIdUpdate.remove(requiredIdUpdate); + } + }) + .doOnTerminate(() -> this.lock.unlock(stamp)); }).then(); } public void markAsAliased(Object aliasEntity, Object entityOrId) { - final long stamp = lock.writeLock(); + final long stamp = this.lock.writeLock(); try { storeProcessedInAlias(aliasEntity, entityOrId); - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } /** - * This returns an id for the given object. We deliberate use the wording of a generic object id as that might either be - * the Neo4j 5+ {@literal elementId()} or on older Neo4j versions or with older data modules {@code toString(id())}. - * - * @param object The object for which an id is requested - * @return The objects id + * This returns an id for the given object. We deliberate use the wording of a generic + * object id as that might either be the Neo4j 5+ {@literal elementId()} or on older + * Neo4j versions or with older data modules {@code toString(id())}. + * @param object the object for which an id is requested + * @return the objects id */ - @Nullable - public Object getObjectId(Object object) { - final long stamp = lock.readLock(); + @Nullable public Object getObjectId(Object object) { + final long stamp = this.lock.readLock(); try { Object valueToCheck = extractRelatedValueFromRelationshipProperties(object); Object possibleId = getProcessedObjectIds(valueToCheck); - return possibleId != null ? possibleId : getProcessedObjectIds(getProcessedAs(valueToCheck)); - } finally { - lock.unlock(stamp); + return (possibleId != null) ? possibleId : getProcessedObjectIds(getProcessedAs(valueToCheck)); + } + finally { + this.lock.unlock(stamp); } } public Object getProcessedAs(Object entity) { - final long stamp = lock.readLock(); + final long stamp = this.lock.readLock(); try { return getProcessedAsWithDefaults(entity); - } finally { - lock.unlock(stamp); + } + finally { + this.lock.unlock(stamp); } } - @Nullable - private Object getProcessedObjectIds(Object entity) { + @Nullable private Object getProcessedObjectIds(Object entity) { if (entity == null) { return null; } - return processedObjectsIds.get(System.identityHashCode(entity)); + return this.processedObjectsIds.get(System.identityHashCode(entity)); } private Object extractRelatedValueFromRelationshipProperties(Object valueToStore) { Object value; if (valueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) { value = ((MappingSupport.RelationshipPropertiesWithEntityHolder) valueToStore).getRelatedEntity(); - } else { + } + else { value = valueToStore; } return value; } /* - * Convenience wrapper functions to avoid exposing the System.identityHashCode "everywhere" in this class. + * Convenience wrapper functions to avoid exposing the System.identityHashCode + * "everywhere" in this class. */ private void storeHashedVersionInProcessedObjectsIds(Object initialObject, Object elementId) { - processedObjectsIds.put(System.identityHashCode(initialObject), elementId); + this.processedObjectsIds.put(System.identityHashCode(initialObject), elementId); } private void storeProcessedInAlias(Object aliasEntity, Object targetEntity) { - processedObjectsAlias.put(System.identityHashCode(aliasEntity), targetEntity); + this.processedObjectsAlias.put(System.identityHashCode(aliasEntity), targetEntity); } private Object getProcessedAsWithDefaults(Object entity) { - return processedObjectsAlias.getOrDefault(System.identityHashCode(entity), entity); + return this.processedObjectsAlias.getOrDefault(System.identityHashCode(entity), entity); } private boolean hasProcessed(Object entity) { - return processedObjectsAlias.containsKey(System.identityHashCode(entity)); + return this.processedObjectsAlias.containsKey(System.identityHashCode(entity)); } private boolean hasProcessedAllOf(Collection entities) { // there can be null elements in the unified collection of values to store. - //noinspection ConstantValue + // noinspection ConstantValue if (entities == null) { return false; } - return processedObjectsIds.keySet().containsAll(entities.stream().map(System::identityHashCode).toList()); + return this.processedObjectsIds.keySet().containsAll(entities.stream().map(System::identityHashCode).toList()); } + + /** + * Valid processing states. + */ + public enum ProcessState { + + /** + * Neither end of relationships has been processed. + */ + PROCESSED_NONE, + /** Both sides of a relationship have been processed. */ + PROCESSED_BOTH, + /** Processed all relationships. */ + PROCESSED_ALL_RELATIONSHIPS, + /** Processed all values. */ + PROCESSED_ALL_VALUES + + } + + /** + * Supplier for arbitrary relationship ids. + */ + @FunctionalInterface + public interface RelationshipIdSupplier { + + Optional getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, + Object toId); + + } + + /** + * Reactive Supplier for arbitrary relationship ids. + */ + @FunctionalInterface + public interface ReactiveRelationshipIdSupplier { + + Mono getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, + Object toId); + + } + + /** + * Combination of relationship description and fromId to differentiate between + * `equals`-wise equal relationship descriptions by their source identifier. This is + * needed because sometimes the very same relationship definition can get processed + * for different objects of the same entity. One could say that this is a Tuple but it + * has a nicer name. + */ + private record RelationshipDescriptionWithSourceId(Object id, RelationshipDescription relationshipDescription) { + } + + private record ProcessedRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder entityHolder, + Object source, Object target, RelationshipDescription relationshipDescription) { + } + + private record RelationshipIdUpdateContext(Statement cypher, Object fromId, Object toId, + NestedRelationshipContext relationshipContext, Object relatedValueToStore, + @Nullable Neo4jPersistentProperty idProperty) { + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java index efa6dbde5..fdcce855f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java @@ -23,16 +23,16 @@ import java.util.Locale; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; + import org.springframework.data.mapping.MappingException; /** - * A {@link NoRootNodeMappingException} is thrown when the entity converter cannot find a node or map like structure - * that can be mapped. - * Nodes eligible for mapping are actual nodes with at least the primary label attached or exactly one map structure - * that is neither a node nor relationship itself. + * A {@link NoRootNodeMappingException} is thrown when the entity converter cannot find a + * node or map like structure that can be mapped. Nodes eligible for mapping are actual + * nodes with at least the primary label attached or exactly one map structure that is + * neither a node nor relationship itself. * * @author Michael J. Simons - * @soundtrack Helge Schneider - Sammlung Schneider! Musik und Lifeshows! * @since 6.0.2 */ @API(status = API.Status.INTERNAL, since = "6.0.2") @@ -43,6 +43,7 @@ public final class NoRootNodeMappingException extends MappingException implement @Nullable private final transient MapAccessor mapAccessor; + @Nullable private final transient Neo4jPersistentEntity entity; @@ -55,13 +56,15 @@ public final class NoRootNodeMappingException extends MappingException implement @Override public void formatTo(Formatter formatter, int flags, int width, int precision) { - if (mapAccessor != null && entity != null) { - String className = entity.getUnderlyingClass().getSimpleName(); - formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", mapAccessor, - className.substring(0, 1).toLowerCase( - Locale.ROOT), String.join(":", entity.getStaticLabels())); - } else { + if (this.mapAccessor != null && this.entity != null) { + String className = this.entity.getUnderlyingClass().getSimpleName(); + formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", this.mapAccessor, + className.substring(0, 1).toLowerCase(Locale.ROOT), + String.join(":", this.entity.getStaticLabels())); + } + else { formatter.format("Could not find mappable nodes or relationships inside a record"); } } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java index 9cbb18f9d..80e0650b4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java @@ -27,31 +27,36 @@ import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Expression; /** - * Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and - * access to the nodes properties. + * Describes how a class is mapped to a node inside the database. It provides navigable + * links to relationships and access to the nodes properties. * + * @param the type of the underlying class * @author Michael J. Simons - * @param The type of the underlying class * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") public interface NodeDescription { /** - * @return The primary label of this entity inside Neo4j. + * Returns the primary label of this entity inside Neo4j. + * @return the primary label of this entity inside Neo4j */ String getPrimaryLabel(); String getMostAbstractParentLabel(NodeDescription mostAbstractNodeDescription); /** - * @return the list of all additional labels (All labels except the {@link NodeDescription#getPrimaryLabel()}). + * Returns the list of all additional labels (All labels except the * + * {@link NodeDescription#getPrimaryLabel()}). + * @return the list of all additional labels */ List getAdditionalLabels(); /** - * @return The list of all static labels, that is the union of {@link #getPrimaryLabel()} + - * {@link #getAdditionalLabels()}. Order is guaranteed to be the primary first, then the others. + * Returns the list of all static labels, that is the union of * + * {@link #getPrimaryLabel()} + {@link #getAdditionalLabels()}. Order is guaranteed to + * * be the primary first, then the others. + * @return the list of all static labels * @since 6.0 */ default List getStaticLabels() { @@ -62,36 +67,46 @@ public interface NodeDescription { } /** - * @return The concrete class to which a node with the given {@link #getPrimaryLabel()} is mapped to + * Returns the concrete class to which a node with the given. + * @return the concrete class to which a node with the given + * {@link #getPrimaryLabel()} is mapped to */ Class getUnderlyingClass(); /** - * @return A description how to determine primary ids for nodes fitting this description + * Returns a description how to determine primary ids for nodes fitting this. + * @return a description how to determine primary ids for nodes fitting this + * description */ - @Nullable - IdDescription getIdDescription(); + @Nullable IdDescription getIdDescription(); /** - * @return A collection of persistent properties that are mapped to graph properties and not to relationships + * Returns a collection of persistent properties that are mapped to graph properties * + * and not to relationships. + * @return a collection of persistent properties that are mapped to graph properties + * and not to relationships */ Collection getGraphProperties(); /** - * @return All graph properties including all properties from the extending classes if this entity is a parent entity. + * Returns all graph properties including all properties from the extending classes if + * * this entity is a parent entity. + * @return all graph properties including all properties from the extending classes if + * this entity is a parent entity. */ Collection getGraphPropertiesInHierarchy(); /** * Retrieves a {@link GraphPropertyDescription} by its field name. - * - * @param fieldName The field name for which the graph property description should be retrieved - * @return An empty optional if there is no property known for the given field. + * @param fieldName the field name for which the graph property description should be + * retrieved + * @return an empty optional if there is no property known for the given field. */ Optional getGraphProperty(String fieldName); /** - * @return True if entities for this node use Neo4j internal ids. + * Returns true if entities for this node use Neo4j internal ids. + * @return true if entities for this node use Neo4j internal ids */ default boolean isUsingInternalIds() { return this.getIdDescription() != null && this.getIdDescription().isInternallyGeneratedId(); @@ -99,56 +114,52 @@ public interface NodeDescription { /** * This returns the outgoing relationships this node has to other nodes. - * - * @return The relationships defined by instances of this node. + * @return the relationships defined by instances of this node */ Collection getRelationships(); /** * This returns the relationships this node, its parent and child has to other nodes. - * - * @param propertyPredicate - Predicate to filter the fields on this node description to - * @return The relationships defined by instances of this node. + * @param propertyPredicate - Predicate to filter the fields on this node description + * to + * @return the relationships defined by instances of this node */ - Collection getRelationshipsInHierarchy(Predicate propertyPredicate); + Collection getRelationshipsInHierarchy( + Predicate propertyPredicate); /** * Register a direct child node description for this entity. - * * @param child - {@link NodeDescription} that defines an extending class. */ void addChildNodeDescription(NodeDescription child); /** * Retrieve all direct child node descriptions which extend this entity. - * * @return all direct child node description. */ Collection> getChildNodeDescriptionsInHierarchy(); + @Nullable NodeDescription getParentNodeDescription(); + /** * Register the direct parent node description. - * * @param parent - {@link NodeDescription} that describes the parent entity. */ void setParentNodeDescription(NodeDescription parent); - @Nullable - NodeDescription getParentNodeDescription(); - /** - * Creates the right identifier expression for this node entity. - * Note: The expression gets cached and won't get recalculated at every invocation. - * - * @return An expression that represents the right identifier type. + * Creates the right identifier expression for this node entity. Note: The expression + * gets cached and won't get recalculated at every invocation. + * @return an expression that represents the right identifier type */ default Expression getIdExpression() { - var idDescription = Objects.requireNonNull(this.getIdDescription(), "No id description available, cannot compute a Cypher expression for retrieving or storing the id"); + var idDescription = Objects.requireNonNull(this.getIdDescription(), + "No id description available, cannot compute a Cypher expression for retrieving or storing the id"); if (idDescription.getOptionalGraphPropertyName() - .flatMap(this::getGraphProperty) - .filter(GraphPropertyDescription::isComposite) - .isPresent()) { + .flatMap(this::getGraphProperty) + .filter(GraphPropertyDescription::isComposite) + .isPresent()) { throw new IllegalStateException("A composite id property cannot be used as ID expression."); } @@ -156,14 +167,18 @@ public interface NodeDescription { } /** - * @param includeField A predicate used to determine the properties that need to be looked at while detecting possible circles. - * @return True if the domain would contain schema circles. + * Checks if the mapping contains possible circles. + * @param includeField a predicate used to determine the properties that need to be + * looked at while detecting possible circles. + * @return true if the domain would contain schema circles. */ boolean containsPossibleCircles(Predicate includeField); /** - * @return True if this persistent entity has been created for an interface. + * Checks if is an entity for an interface. + * @return true if this persistent entity has been created for an interface * @since 6.0.8 */ boolean describesInterface(); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionAndLabels.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionAndLabels.java index 9bc701e49..22bb28922 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionAndLabels.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionAndLabels.java @@ -18,12 +18,12 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Collection; /** - * Wraps a resolved node description together with the complete list of labels returned from the database and the list - * of labels not statically defined in the resolved node hierarchy. + * Wraps a resolved node description together with the complete list of labels returned + * from the database and the list of labels not statically defined in the resolved node + * hierarchy. * * @author Michael J. Simons * @since 6.0 - * @soundtrack The Rolling Stones - Living In A Ghost Town */ final class NodeDescriptionAndLabels { @@ -36,11 +36,12 @@ final class NodeDescriptionAndLabels { this.dynamicLabels = dynamicLabels; } - public NodeDescription getNodeDescription() { - return nodeDescription; + NodeDescription getNodeDescription() { + return this.nodeDescription; } - public Collection getDynamicLabels() { - return dynamicLabels; + Collection getDynamicLabels() { + return this.dynamicLabels; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java index b847163c4..088154f84 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java @@ -29,11 +29,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.context.AbstractMappingContext; /** - * This class is more or less just a wrapper around the node description lookup map. It ensures that there is no cyclic - * dependency between {@link Neo4jMappingContext} and {@link DefaultNeo4jEntityConverter}. + * This class is more or less just a wrapper around the node description lookup map. It + * ensures that there is no cyclic dependency between {@link Neo4jMappingContext} and + * {@link DefaultNeo4jEntityConverter}. * * @author Gerrit Meier * @author Michael J. Simons @@ -41,56 +43,56 @@ import org.springframework.data.mapping.context.AbstractMappingContext; final class NodeDescriptionStore { /** - * A lookup of entities based on their primary label. We depend on the locking mechanism provided by the - * {@link AbstractMappingContext}, so this lookup is not synchronized further. + * A lookup of entities based on their primary label. We depend on the locking + * mechanism provided by the {@link AbstractMappingContext}, so this lookup is not + * synchronized further. */ private final Map> nodeDescriptionsByPrimaryLabel = new ConcurrentHashMap<>(); private final Map, Map, NodeDescriptionAndLabels>> nodeDescriptionAndLabelsCache = new ConcurrentHashMap<>(); - private final BiFunction, List, NodeDescriptionAndLabels> nodeDescriptionAndLabels = - (nodeDescription, labels) -> { - Map, NodeDescriptionAndLabels> listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription); - if (listNodeDescriptionAndLabelsMap == null) { - nodeDescriptionAndLabelsCache.put(nodeDescription, new ConcurrentHashMap<>()); - listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription); - } + private final BiFunction, List, NodeDescriptionAndLabels> nodeDescriptionAndLabels = ( + nodeDescription, labels) -> { + Map, NodeDescriptionAndLabels> listNodeDescriptionAndLabelsMap = this.nodeDescriptionAndLabelsCache + .get(nodeDescription); + if (listNodeDescriptionAndLabelsMap == null) { + this.nodeDescriptionAndLabelsCache.put(nodeDescription, new ConcurrentHashMap<>()); + listNodeDescriptionAndLabelsMap = this.nodeDescriptionAndLabelsCache.get(nodeDescription); + } - NodeDescriptionAndLabels cachedNodeDescriptionAndLabels = listNodeDescriptionAndLabelsMap.get(labels); - if (cachedNodeDescriptionAndLabels == null) { - cachedNodeDescriptionAndLabels = computeConcreteNodeDescription(nodeDescription, labels); - listNodeDescriptionAndLabelsMap.put(labels, cachedNodeDescriptionAndLabels); - } - return cachedNodeDescriptionAndLabels; - }; + NodeDescriptionAndLabels cachedNodeDescriptionAndLabels = listNodeDescriptionAndLabelsMap.get(labels); + if (cachedNodeDescriptionAndLabels == null) { + cachedNodeDescriptionAndLabels = computeConcreteNodeDescription(nodeDescription, labels); + listNodeDescriptionAndLabelsMap.put(labels, cachedNodeDescriptionAndLabels); + } + return cachedNodeDescriptionAndLabels; + }; - public boolean containsKey(String primaryLabel) { - return nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel); + boolean containsKey(String primaryLabel) { + return this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel); } - public boolean containsValue(DefaultNeo4jPersistentEntity newEntity) { - return nodeDescriptionsByPrimaryLabel.containsValue(newEntity); + boolean containsValue(DefaultNeo4jPersistentEntity newEntity) { + return this.nodeDescriptionsByPrimaryLabel.containsValue(newEntity); } - public void put(String primaryLabel, DefaultNeo4jPersistentEntity newEntity) { - nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); + void put(String primaryLabel, DefaultNeo4jPersistentEntity newEntity) { + this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); } - public Set>> entrySet() { - return nodeDescriptionsByPrimaryLabel.entrySet(); + Set>> entrySet() { + return this.nodeDescriptionsByPrimaryLabel.entrySet(); } - public Collection> values() { - return nodeDescriptionsByPrimaryLabel.values(); + Collection> values() { + return this.nodeDescriptionsByPrimaryLabel.values(); } - @Nullable - public NodeDescription get(String primaryLabel) { - return nodeDescriptionsByPrimaryLabel.get(primaryLabel); + @Nullable NodeDescription get(String primaryLabel) { + return this.nodeDescriptionsByPrimaryLabel.get(primaryLabel); } - @Nullable - public NodeDescription getNodeDescription(Class targetType) { + @Nullable NodeDescription getNodeDescription(Class targetType) { for (NodeDescription nodeDescription : values()) { if (nodeDescription.getUnderlyingClass().equals(targetType)) { return nodeDescription; @@ -99,13 +101,16 @@ final class NodeDescriptionStore { return null; } - public NodeDescriptionAndLabels deriveConcreteNodeDescription(NodeDescription entityDescription, List labels) { - return nodeDescriptionAndLabels.apply(entityDescription, labels); + NodeDescriptionAndLabels deriveConcreteNodeDescription(NodeDescription entityDescription, List labels) { + return this.nodeDescriptionAndLabels.apply(entityDescription, labels); } - private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription entityDescription, List labels) { + private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription entityDescription, + List labels) { - boolean isConcreteClassThatFulfillsEverything = !Modifier.isAbstract(entityDescription.getUnderlyingClass().getModifiers()) && entityDescription.getStaticLabels().containsAll(labels); + boolean isConcreteClassThatFulfillsEverything = !Modifier + .isAbstract(entityDescription.getUnderlyingClass().getModifiers()) + && entityDescription.getStaticLabels().containsAll(labels); if (labels == null || labels.isEmpty() || isConcreteClassThatFulfillsEverything) { return new NodeDescriptionAndLabels(entityDescription, Collections.emptyList()); @@ -114,7 +119,8 @@ final class NodeDescriptionStore { Collection> haystack; if (entityDescription.describesInterface()) { haystack = this.values(); - } else { + } + else { haystack = entityDescription.getChildNodeDescriptionsInHierarchy(); } @@ -143,13 +149,15 @@ final class NodeDescriptionStore { for (String label : labels) { if (staticLabels.contains(label)) { matchingLabels.add(label); - } else { + } + else { unmatchedLabelsCount++; } } unmatchedLabelsCache.put(nd, unmatchedLabelsCount); - if (mostMatchingNodeDescription == null || unmatchedLabelsCount < Objects.requireNonNullElse(unmatchedLabelsCache.get(mostMatchingNodeDescription), Integer.MAX_VALUE)) { + if (mostMatchingNodeDescription == null || unmatchedLabelsCount < Objects + .requireNonNullElse(unmatchedLabelsCache.get(mostMatchingNodeDescription), Integer.MAX_VALUE)) { mostMatchingNodeDescription = nd; mostMatchingStaticLabels = matchingLabels; } @@ -160,7 +168,9 @@ final class NodeDescriptionStore { mostMatchingStaticLabels.forEach(surplusLabels::remove); } if (mostMatchingNodeDescription == null) { - throw new IllegalStateException("Could not compute a concrete node description for entity %s and labels %s".formatted(entityDescription, labels)); + throw new IllegalStateException( + "Could not compute a concrete node description for entity %s and labels %s" + .formatted(entityDescription, labels)); } return new NodeDescriptionAndLabels(mostMatchingNodeDescription, surplusLabels); } @@ -170,4 +180,5 @@ final class NodeDescriptionStore { entityDescription.getAdditionalLabels().forEach(surplusLabels::remove); return new NodeDescriptionAndLabels(entityDescription, surplusLabels); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java index 8ad11287a..b29dfa9a1 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NullSafeNeo4jPersistentPropertyConverter.java @@ -18,15 +18,15 @@ package org.springframework.data.neo4j.core.mapping; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; /** - * All property converters will be wrapped by this class. It adds the information if a converter needs to be applied to - * a complete collection or to individual values. + * All property converters will be wrapped by this class. It adds the information if a + * converter needs to be applied to a complete collection or to individual values. * + * @param the type of the property this converter converts. * @author Michael J. Simons - * @param The type of the property this converter converts. - * @soundtrack Roger Taylor - The Outsider */ final class NullSafeNeo4jPersistentPropertyConverter implements Neo4jPersistentPropertyConverter { @@ -36,7 +36,8 @@ final class NullSafeNeo4jPersistentPropertyConverter implements Neo4jPersiste private final Neo4jPersistentPropertyConverter delegate; /** - * {@literal false} for all non-composite converters. If true, {@literal null} will be passed to the writing converter + * {@literal false} for all non-composite converters. If true, {@literal null} will be + * passed to the writing converter */ private final boolean passNullOnWrite; @@ -55,18 +56,18 @@ final class NullSafeNeo4jPersistentPropertyConverter implements Neo4jPersiste @Override public Value write(@Nullable T source) { if (source == null) { - return passNullOnWrite ? delegate.write(source) : Values.NULL; + return this.passNullOnWrite ? this.delegate.write(source) : Values.NULL; } - return delegate.write(source); + return this.delegate.write(source); } @Override - @Nullable - public T read(@Nullable Value source) { - return source == null || source.isNull() ? null : delegate.read(source); + @Nullable public T read(@Nullable Value source) { + return (source == null || source.isNull()) ? null : this.delegate.read(source); } - public boolean isForCollection() { - return forCollection; + boolean isForCollection() { + return this.forCollection; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java index c49dbb45f..e0384e0b6 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristics.java @@ -15,41 +15,26 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.apiguardian.api.API.Status.STABLE; - import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import static org.apiguardian.api.API.Status.STABLE; + /** - * The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by default derived from the annotated - * classes. Diverging characteristics are requested by the {@link Neo4jMappingContext} prior to creating a persistent property. - * Additional providers of characteristics may be registered with the mapping context. + * The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by + * default derived from the annotated classes. Diverging characteristics are requested by + * the {@link Neo4jMappingContext} prior to creating a persistent property. Additional + * providers of characteristics may be registered with the mapping context. * * @author Michael J. Simons - * @soundtrack Metallica - Kill 'Em All * @since 6.3.7 */ @API(status = STABLE, since = "6.3.7") public interface PersistentPropertyCharacteristics { /** - * @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise - */ - @Nullable - default Boolean isTransient() { - return null; - } - - /** - * @return {@literal null} to leave the defaults, {@literal true} or {@literal false} otherwise - */ - @Nullable - default Boolean isReadOnly() { - return null; - } - - /** - * @return Characteristics applying the defaults + * Default characteristics. + * @return characteristics applying the defaults */ static PersistentPropertyCharacteristics useDefaults() { return new PersistentPropertyCharacteristics() { @@ -57,7 +42,8 @@ public interface PersistentPropertyCharacteristics { } /** - * @return Characteristics to treat a property as transient + * Treats the property to which the instance is applied as transient. + * @return characteristics to treat a property as transient */ static PersistentPropertyCharacteristics treatAsTransient() { return new PersistentPropertyCharacteristics() { @@ -69,7 +55,8 @@ public interface PersistentPropertyCharacteristics { } /** - * @return Characteristics to treat a property as transient + * Treats the property to which the instance is applied as read only. + * @return characteristics to treat a property as read only */ static PersistentPropertyCharacteristics treatAsReadOnly() { return new PersistentPropertyCharacteristics() { @@ -79,4 +66,23 @@ public interface PersistentPropertyCharacteristics { } }; } + + /** + * Return {@literal true} to mark a property as transient. + * @return {@literal null} to leave the defaults, {@literal true} or {@literal false} + * otherwise + */ + @Nullable default Boolean isTransient() { + return null; + } + + /** + * Return {@literal true} to mark a property as read only. + * @return {@literal null} to leave the defaults, {@literal true} or {@literal false} + * otherwise + */ + @Nullable default Boolean isReadOnly() { + return null; + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristicsProvider.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristicsProvider.java index 1220d1478..b10317ea3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristicsProvider.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PersistentPropertyCharacteristicsProvider.java @@ -15,21 +15,24 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.apiguardian.api.API.Status.STABLE; - import java.util.function.BiFunction; import org.apiguardian.api.API; + import org.springframework.data.mapping.model.Property; +import static org.apiguardian.api.API.Status.STABLE; + /** - * An instance of such a provider can be registered as a Spring bean and will be consulted by the {@link Neo4jMappingContext} - * prior to creating and populating {@link Neo4jPersistentProperty persistent properties}. + * An instance of such a provider can be registered as a Spring bean and will be consulted + * by the {@link Neo4jMappingContext} prior to creating and populating + * {@link Neo4jPersistentProperty persistent properties}. * * @author Michael J. Simons - * @soundtrack Metallica - Kill 'Em All * @since 6.3.7 */ @API(status = STABLE, since = "6.3.7") -public interface PersistentPropertyCharacteristicsProvider extends BiFunction, PersistentPropertyCharacteristics> { +public interface PersistentPropertyCharacteristicsProvider + extends BiFunction, PersistentPropertyCharacteristics> { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java index ae34227de..8197e8647 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java @@ -23,15 +23,25 @@ import java.util.function.Predicate; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.neo4j.core.schema.Property; import org.springframework.util.StringUtils; /** - * Something that makes sense of propertyPaths by having an understanding of projection classes. + * Something that makes sense of propertyPaths by having an understanding of projection + * classes. + * + * @author Michael J. Simons + * @author Gerrit Meier */ @API(status = API.Status.INTERNAL) public abstract class PropertyFilter { + /** + * A default predicate that does not filter anything. + */ + public static final Predicate NO_FILTER = (pp) -> true; + public static PropertyFilter from(Collection projectedPaths, NodeDescription nodeDescription) { return new FilteringPropertyFilter(projectedPaths, nodeDescription); } @@ -40,14 +50,6 @@ public abstract class PropertyFilter { return new NonFilteringPropertyFilter(); } - public static final Predicate NO_FILTER = (pp) -> true; - - public abstract boolean contains(String dotPath, Class typeToCheck); - - public abstract boolean contains(RelaxedPropertyPath propertyPath); - - public abstract boolean isNotFiltering(); - static String toDotPath(RelaxedPropertyPath propertyPath, String lastSegment) { if (lastSegment == null) { @@ -56,40 +58,46 @@ public abstract class PropertyFilter { return propertyPath.replaceLastSegment(lastSegment).toDotPath(); } - private static class FilteringPropertyFilter extends PropertyFilter { + public abstract boolean contains(String dotPath, Class typeToCheck); + + public abstract boolean contains(RelaxedPropertyPath propertyPath); + + public abstract boolean isNotFiltering(); + + private static final class FilteringPropertyFilter extends PropertyFilter { + private final Set> rootClasses; + private final Collection projectingPropertyPaths; private FilteringPropertyFilter(Collection projectedPaths, NodeDescription nodeDescription) { Class domainClass = nodeDescription.getUnderlyingClass(); - rootClasses = new HashSet<>(); - rootClasses.add(domainClass); + this.rootClasses = new HashSet<>(); + this.rootClasses.add(domainClass); // supported projection based classes - projectedPaths.stream().map(property -> property.propertyPath.getType()).forEach(rootClasses::add); + projectedPaths.stream().map(property -> property.propertyPath.getType()).forEach(this.rootClasses::add); // supported inheriting classes - nodeDescription.getChildNodeDescriptionsInHierarchy().stream() - .map(NodeDescription::getUnderlyingClass) - .forEach(rootClasses::add); + nodeDescription.getChildNodeDescriptionsInHierarchy() + .stream() + .map(NodeDescription::getUnderlyingClass) + .forEach(this.rootClasses::add); Neo4jPersistentEntity entity = (Neo4jPersistentEntity) nodeDescription; - projectingPropertyPaths = new HashSet<>(); - projectedPaths - .forEach(propertyPath -> { - String lastSegment = null; + this.projectingPropertyPaths = new HashSet<>(); + projectedPaths.forEach(propertyPath -> { + String lastSegment = null; - Neo4jPersistentProperty property = entity.getPersistentProperty(propertyPath.propertyPath.dotPath); - if (property != null && property.findAnnotation(Property.class) != null) { - lastSegment = property.getPropertyName(); - } + Neo4jPersistentProperty property = entity.getPersistentProperty(propertyPath.propertyPath.dotPath); + if (property != null && property.findAnnotation(Property.class) != null) { + lastSegment = property.getPropertyName(); + } - projectingPropertyPaths.add(new ProjectedPath( - propertyPath.propertyPath.replaceLastSegment(lastSegment), - propertyPath.isEntity) - ); - }); + this.projectingPropertyPaths.add(new ProjectedPath( + propertyPath.propertyPath.replaceLastSegment(lastSegment), propertyPath.isEntity)); + }); } @Override @@ -98,25 +106,26 @@ public abstract class PropertyFilter { return true; } - if (!rootClasses.contains(typeToCheck)) { + if (!this.rootClasses.contains(typeToCheck)) { return false; } // create a sorted list of the deepest paths first - Optional candidate = projectingPropertyPaths.stream() - .filter(pp -> pp.isEntity) - .map(pp -> pp.propertyPath.toDotPath()).sorted((o1, o2) -> { - int depth1 = StringUtils.countOccurrencesOf(o1, "."); - int depth2 = StringUtils.countOccurrencesOf(o2, "."); + Optional candidate = this.projectingPropertyPaths.stream() + .filter(pp -> pp.isEntity) + .map(pp -> pp.propertyPath.toDotPath()) + .sorted((o1, o2) -> { + int depth1 = StringUtils.countOccurrencesOf(o1, "."); + int depth2 = StringUtils.countOccurrencesOf(o2, "."); - return Integer.compare(depth2, depth1); - }) - .filter(d -> dotPath.contains(d) && dotPath.startsWith(d)) - .findFirst(); + return Integer.compare(depth2, depth1); + }) + .filter(d -> dotPath.contains(d) && dotPath.startsWith(d)) + .findFirst(); - return projectingPropertyPaths.stream().map(pp -> pp.propertyPath.toDotPath()) - .anyMatch(ppDotPath -> ppDotPath.equals(dotPath)) - || (dotPath.contains(".") && candidate.isPresent()); + return this.projectingPropertyPaths.stream() + .map(pp -> pp.propertyPath.toDotPath()) + .anyMatch(ppDotPath -> ppDotPath.equals(dotPath)) || (dotPath.contains(".") && candidate.isPresent()); } @Override @@ -126,11 +135,12 @@ public abstract class PropertyFilter { @Override public boolean isNotFiltering() { - return projectingPropertyPaths.isEmpty(); + return this.projectingPropertyPaths.isEmpty(); } + } - private static class NonFilteringPropertyFilter extends PropertyFilter { + private static final class NonFilteringPropertyFilter extends PropertyFilter { @Override public boolean contains(String dotPath, Class typeToCheck) { @@ -146,25 +156,33 @@ public abstract class PropertyFilter { public boolean isNotFiltering() { return true; } + } /** - * A very loose coupling between a dot path and its (possible) owning type. - * This is due to the fact that the original PropertyPath does throw an exception on creation when a property - * is not found on the entity. - * Since we are supporting also querying for base classes with properties coming from the inheriting classes, - * this test on creation is too strict. + * A very loose coupling between a dot path and its (possible) owning type. This is + * due to the fact that the original PropertyPath does throw an exception on creation + * when a property is not found on the entity. Since we are supporting also querying + * for base classes with properties coming from the inheriting classes, this test on + * creation is too strict. */ - public static class RelaxedPropertyPath { + public static final class RelaxedPropertyPath { + private final String dotPath; + private final Class type; + private RelaxedPropertyPath(String dotPath, Class type) { + this.dotPath = dotPath; + this.type = type; + } + public static RelaxedPropertyPath withRootType(Class type) { return new RelaxedPropertyPath("", type); } public String toDotPath() { - return dotPath; + return this.dotPath; } public String toDotPath(String lastSegment) { @@ -173,20 +191,15 @@ public abstract class PropertyFilter { return this.toDotPath(); } - int idx = dotPath.lastIndexOf('.'); + int idx = this.dotPath.lastIndexOf('.'); if (idx < 0) { return lastSegment; } - return dotPath.substring(0, idx + 1) + lastSegment; + return this.dotPath.substring(0, idx + 1) + lastSegment; } public Class getType() { - return type; - } - - private RelaxedPropertyPath(String dotPath, Class type) { - this.dotPath = dotPath; - this.type = type; + return this.type; } public RelaxedPropertyPath append(String pathPart) { @@ -198,50 +211,56 @@ public abstract class PropertyFilter { } private String appendToDotPath(String pathPart) { - return dotPath.isEmpty() ? pathPart : dotPath + "." + pathPart; + return this.dotPath.isEmpty() ? pathPart : this.dotPath + "." + pathPart; } private String prependDotPathWith(String pathPart) { - return dotPath.isEmpty() ? pathPart : pathPart + "." + dotPath; + return this.dotPath.isEmpty() ? pathPart : pathPart + "." + this.dotPath; } public String getSegment() { - int idx = dotPath.indexOf("."); + int idx = this.dotPath.indexOf("."); if (idx < 0) { - idx = dotPath.length(); + idx = this.dotPath.length(); } - return dotPath.substring(0, idx); + return this.dotPath.substring(0, idx); } public RelaxedPropertyPath getLeafProperty() { - int idx = dotPath.lastIndexOf('.'); + int idx = this.dotPath.lastIndexOf('.'); if (idx < 0) { return this; } - return new RelaxedPropertyPath(dotPath.substring(idx + 1), this.type); + return new RelaxedPropertyPath(this.dotPath.substring(idx + 1), this.type); } public RelaxedPropertyPath replaceLastSegment(@Nullable String lastSegment) { if (lastSegment == null) { return this; } - return new RelaxedPropertyPath(getSegment().equals(dotPath) ? lastSegment : getSegment() + "." + lastSegment, type); + return new RelaxedPropertyPath( + getSegment().equals(this.dotPath) ? lastSegment : getSegment() + "." + lastSegment, this.type); } + } /** * Wrapper class for property paths and information if they point to an entity. */ public static class ProjectedPath { + final RelaxedPropertyPath propertyPath; + final boolean isEntity; public ProjectedPath(RelaxedPropertyPath propertyPath, boolean isEntity) { this.propertyPath = propertyPath; this.isEntity = isEntity; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyHandlerSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyHandlerSupport.java index 1361d4cf6..d2b1bd992 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyHandlerSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyHandlerSupport.java @@ -19,12 +19,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apiguardian.api.API; + import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * Warning Internal API, might change without further notice, even in patch releases. + * Warning Internal API, might change without further notice, even in + * patch releases. *

* This class adds {@link TargetNode @TargetNode} properties back to properties. * @@ -34,11 +36,7 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @API(status = API.Status.INTERNAL, since = "6.3") public final class PropertyHandlerSupport { - private final static Map, PropertyHandlerSupport> CACHE = new ConcurrentHashMap<>(); - - public static PropertyHandlerSupport of(Neo4jPersistentEntity entity) { - return CACHE.computeIfAbsent(entity, PropertyHandlerSupport::new); - } + private static final Map, PropertyHandlerSupport> CACHE = new ConcurrentHashMap<>(); private final Neo4jPersistentEntity entity; @@ -46,13 +44,18 @@ public final class PropertyHandlerSupport { this.entity = entity; } + public static PropertyHandlerSupport of(Neo4jPersistentEntity entity) { + return CACHE.computeIfAbsent(entity, PropertyHandlerSupport::new); + } + public Neo4jPersistentEntity doWithProperties(PropertyHandler handler) { - entity.doWithProperties(handler); - entity.doWithAssociations((AssociationHandler) association -> { + this.entity.doWithProperties(handler); + this.entity.doWithAssociations((AssociationHandler) association -> { if (association.getInverse().isAnnotationPresent(TargetNode.class)) { handler.doWithPersistentProperty(association.getInverse()); } }); - return entity; + return this.entity; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java index 15db276d9..b6ec08d88 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyTraverser.java @@ -24,14 +24,16 @@ import java.util.function.BiPredicate; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * A strategy for traversing all properties (including association) once, without going in circles with cyclic mappings. - * Uses the same idea of relationship isomorphism like Cypher does (Relationship isomorphism means that one relationship - * or association cannot be returned more than once for each entity). + * A strategy for traversing all properties (including association) once, without going in + * circles with cyclic mappings. Uses the same idea of relationship isomorphism like + * Cypher does (Relationship isomorphism means that one relationship or association cannot + * be returned more than once for each entity). * * @author Michael J. Simons * @since 6.3 @@ -40,40 +42,32 @@ import org.springframework.data.neo4j.core.schema.TargetNode; public final class PropertyTraverser { private final Neo4jMappingContext ctx; + private final Set> pathsTraversed = new HashSet<>(); public PropertyTraverser(Neo4jMappingContext ctx) { this.ctx = ctx; } - public void traverse( - Class root, - BiConsumer sink - ) { + public void traverse(Class root, BiConsumer sink) { traverse(root, (path, toProperty) -> true, sink); } - public synchronized void traverse( - Class root, - BiPredicate predicate, - BiConsumer sink - ) { + public synchronized void traverse(Class root, BiPredicate predicate, + BiConsumer sink) { this.pathsTraversed.clear(); - traverseImpl(ctx.getRequiredPersistentEntity(root), null, predicate, sink, false); + traverseImpl(this.ctx.getRequiredPersistentEntity(root), null, predicate, sink, false); } - private void traverseImpl( - Neo4jPersistentEntity root, - @Nullable PropertyPath base, + private void traverseImpl(Neo4jPersistentEntity root, @Nullable PropertyPath base, BiPredicate predicate, - BiConsumer sink, - boolean pathAlreadyVisited - ) { - Set sortedProperties = new TreeSet<>(Comparator.comparing(Neo4jPersistentProperty::getName)); + BiConsumer sink, boolean pathAlreadyVisited) { + Set sortedProperties = new TreeSet<>( + Comparator.comparing(Neo4jPersistentProperty::getName)); root.doWithAll(sortedProperties::add); sortedProperties.forEach(p -> { - PropertyPath path = - base == null ? PropertyPath.from(p.getName(), p.getOwner().getType()) : base.nested(p.getName()); + PropertyPath path = (base != null) ? base.nested(p.getName()) + : PropertyPath.from(p.getName(), p.getOwner().getType()); if (!predicate.test(path, p)) { return; @@ -86,11 +80,12 @@ public final class PropertyTraverser { return; } - Neo4jPersistentEntity targetEntity = ctx.getRequiredPersistentEntity(associationTargetType); - boolean recalledForProperties = pathsTraversed.contains(p.getAssociation()); - pathsTraversed.add(p.getAssociation()); + Neo4jPersistentEntity targetEntity = this.ctx.getRequiredPersistentEntity(associationTargetType); + boolean recalledForProperties = this.pathsTraversed.contains(p.getAssociation()); + this.pathsTraversed.add(p.getAssociation()); traverseImpl(targetEntity, path, predicate, sink, recalledForProperties); } }); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java index 9d2b6728c..f3752e5e9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/RelationshipDescription.java @@ -20,12 +20,13 @@ import java.util.Optional; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.neo4j.core.schema.Relationship; /** - * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction is - * maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by - * {@link #getTarget}. + * Description of a relationship. Those descriptions always describe outgoing + * relationships. The inverse direction is maybe defined on the {@link NodeDescription} + * reachable in the {@link Schema} via it's primary label defined by {@link #getTarget}. * * @author Michael J. Simons * @since 6.0 @@ -33,82 +34,89 @@ import org.springframework.data.neo4j.core.schema.Relationship; @API(status = API.Status.INTERNAL, since = "6.0") public interface RelationshipDescription { + /** + * The name of the property SDN uses to transport relationship types. + */ String NAME_OF_RELATIONSHIP = "__relationship__"; + /** + * The name of the property SDN uses to transport relationship types. + */ String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__"; /** - * If this relationship is dynamic, then this method always returns the name of the inverse property. - * - * @return The type of this relationship + * If this relationship is dynamic, then this method always returns the name of the + * inverse property. + * @return the type of this relationship */ String getType(); /** * A relationship is dynamic when it's modelled as a {@code Map}. - * - * @return True, if this relationship is dynamic + * @return true, if this relationship is dynamic */ boolean isDynamic(); /** - * The source of this relationship is described by the primary label of the node in question. - * - * @return The source of this relationship + * The source of this relationship is described by the primary label of the node in + * question. + * @return the source of this relationship */ NodeDescription getSource(); /** - * The target of this relationship is described by the primary label of the node in question. - * If the relationship description includes a relationship properties class, this will be the {@link NodeDescription} - * of the {@link org.springframework.data.neo4j.core.schema.TargetNode}. - * - * @return The target of this relationship + * The target of this relationship is described by the primary label of the node in + * question. If the relationship description includes a relationship properties class, + * this will be the {@link NodeDescription} of the + * {@link org.springframework.data.neo4j.core.schema.TargetNode}. + * @return the target of this relationship */ NodeDescription getTarget(); /** - * The name of the property where the relationship was defined. This is used by the Cypher creation to name the return - * values. - * - * @return The name of the field storing the relationship property + * The name of the property where the relationship was defined. This is used by the + * Cypher creation to name the return values. + * @return the name of the field storing the relationship property */ String getFieldName(); /** - * The direction of the defined relationship. This is used by the Cypher creation to query for relationships and - * create them with the right directions. - * - * @return The direction of the relationship + * The direction of the defined relationship. This is used by the Cypher creation to + * query for relationships and create them with the right directions. + * @return the direction of the relationship */ Relationship.Direction getDirection(); /** - * If this is a relationship with properties, the properties-defining class will get returned, otherwise - * {@literal null}. - * - * @return The type of the relationship property class for relationship with properties, otherwise {@literal null} + * If this is a relationship with properties, the properties-defining class will get + * returned, otherwise {@literal null}. + * @return the type of the relationship property class for relationship with + * properties, otherwise {@literal null} */ - @Nullable - NodeDescription getRelationshipPropertiesEntity(); + @Nullable NodeDescription getRelationshipPropertiesEntity(); default NodeDescription getRequiredRelationshipPropertiesEntity() { - return Objects.requireNonNull(getRelationshipPropertiesEntity(), () -> "Relationship entity %s does not point to an entity holding the relationships' properties".formatted(this.getType())); + return Objects.requireNonNull(getRelationshipPropertiesEntity(), + () -> "Relationship entity %s does not point to an entity holding the relationships' properties" + .formatted(this.getType())); } /** - * Tells if this relationship is a relationship with additional properties. In such cases - * {@code getRelationshipPropertiesClass} will return the type of the properties holding class. - * - * @return {@literal true} if an additional properties are available, otherwise {@literal false} + * Tells if this relationship is a relationship with additional properties. In such + * cases {@code getRelationshipPropertiesClass} will return the type of the properties + * holding class. + * @return {@literal true} if an additional properties are available, otherwise + * {@literal false} */ boolean hasRelationshipProperties(); default boolean hasInternalIdProperty() { - return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity()).map(NodeDescription::getIdDescription) - .filter(IdDescription::isInternallyGeneratedId).isPresent(); + return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity()) + .map(NodeDescription::getIdDescription) + .filter(IdDescription::isInternallyGeneratedId) + .isPresent(); } default boolean isOutgoing() { @@ -121,31 +129,36 @@ public interface RelationshipDescription { default String generateRelatedNodesCollectionName(NodeDescription mostAbstractNodeDescription) { - return this.getSource().getMostAbstractParentLabel(mostAbstractNodeDescription) + "_" + this.getType() + "_" + this.getTarget().getPrimaryLabel() + "_" + this.isOutgoing(); + return this.getSource().getMostAbstractParentLabel(mostAbstractNodeDescription) + "_" + this.getType() + "_" + + this.getTarget().getPrimaryLabel() + "_" + this.isOutgoing(); } /** - * Set the relationship definition that describes the opposite side of the relationship. - * - * @param relationshipObverse logically same relationship definition in the target entity + * Returns the logically same relationship definition in the target entity. + * @return logically same relationship definition in the target entity + */ + @Nullable RelationshipDescription getRelationshipObverse(); + + /** + * Set the relationship definition that describes the opposite side of the + * relationship. + * @param relationshipObverse logically same relationship definition in the target + * entity */ void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse); /** - * @return logically same relationship definition in the target entity - */ - @Nullable - RelationshipDescription getRelationshipObverse(); - - /** - * Checks if there is a relationship description describing the obverse of this relationship. - * - * @return true if a logically same relationship in the target entity exists, otherwise false. + * Checks if there is a relationship description describing the obverse of this + * relationship. + * @return true if a logically same relationship in the target entity exists, + * otherwise false. */ boolean hasRelationshipObverse(); /** - * {@return true if updates should be cascaded along this relationship} + * Returns true if updates should be cascaded along this relationship. + * @return true if updates should be cascaded along this relationship */ boolean cascadeUpdates(); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java index 86684805e..75195f85c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java @@ -25,11 +25,13 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.schema.IdGenerator; /** - * Contains the descriptions of all nodes, their properties and relationships known to SDN. + * Contains the descriptions of all nodes, their properties and relationships known to + * SDN. * * @author Michael J. Simons * @since 6.0 @@ -39,21 +41,17 @@ public interface Schema { /** * Retrieves a node's description by its primary label. - * - * @param primaryLabel The primary label under which the node is described - * @return The description if any, null otherwise + * @param primaryLabel the primary label under which the node is described + * @return the description if any, null otherwise */ - @Nullable - NodeDescription getNodeDescription(String primaryLabel); + @Nullable NodeDescription getNodeDescription(String primaryLabel); /** * Retrieves a node's description by its underlying class. - * - * @param underlyingClass The underlying class of the node description to be retrieved - * @return The description if any, null otherwise + * @param underlyingClass the underlying class of the node description to be retrieved + * @return the description if any, null otherwise */ - @Nullable - NodeDescription getNodeDescription(Class underlyingClass); + @Nullable NodeDescription getNodeDescription(Class underlyingClass); default NodeDescription getRequiredNodeDescription(Class underlyingClass) { NodeDescription nodeDescription = getNodeDescription(underlyingClass); @@ -73,19 +71,21 @@ public interface Schema { } /** - * Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a record - * containing all the nodes and relationships necessary to fully populate an instance of the given class. It will not - * try to fetch data from any other records or queries. The mapping function is free to throw a - * {@link RuntimeException}, most likely a {@code org.springframework.data.mapping.MappingException} or + * Retrieves a schema based mapping function for the {@code targetClass}. The mapping + * function will expect a record containing all the nodes and relationships necessary + * to fully populate an instance of the given class. It will not try to fetch data + * from any other records or queries. The mapping function is free to throw a + * {@link RuntimeException}, most likely a + * {@code org.springframework.data.mapping.MappingException} or * {@link IllegalStateException} when mapping is not possible. *

- * In case the mapping function returns a {@literal null}, the Neo4j client will throw an exception and prevent - * further processing. - * - * @param targetClass The target class to which to map to. - * @param Type of the target class - * @return The default, stateless and reusable mapping function for the given target class - * @throws UnknownEntityException When {@code targetClass} is not a managed class + * In case the mapping function returns a {@literal null}, the Neo4j client will throw + * an exception and prevent further processing. + * @param targetClass the target class to which to map to. + * @param the type of the target class + * @return the default, stateless and reusable mapping function for the given target + * class + * @throws UnknownEntityException when {@code targetClass} is not a managed class */ default BiFunction getRequiredMappingFunctionFor(Class targetClass) { NodeDescription nodeDescription = getNodeDescription(targetClass); @@ -96,14 +96,17 @@ public interface Schema { return (typeSystem, record) -> { try { return entityConverter.read(targetClass, record); - } catch (IllegalStateException ex) { + } + catch (IllegalStateException ex) { return null; } }; } /** - * @return The (reading and writing) converter used to read records into entities and write entities into maps. + * Returns the (reading and writing) converter used to read records into entities and + * write entities into maps. + * @return the entity converter for the schema */ Neo4jEntityConverter getEntityConverter(); @@ -122,13 +125,15 @@ public interface Schema { } /** - * Creates or retrieves an instance of the given id generator class. During the lifetime of the schema, this method - * returns the same instance of reoccurring requests of the same type. - * - * @param idGeneratorType The type of the ID generator to return - * @return The id generator. + * Creates or retrieves an instance of the given id generator class. During the + * lifetime of the schema, this method returns the same instance of reoccurring + * requests of the same type. + * @param idGeneratorType the type of the ID generator to return + * @param the class type + * @return the id generator. */ > T getOrCreateIdGeneratorOfType(Class idGeneratorType); > Optional getIdGenerator(String reference); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java b/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java index 83f50907d..d2fb37ed0 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.function.Function; + import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.FunctionInvocation; @@ -23,8 +25,6 @@ import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.Relationship; import org.neo4j.cypherdsl.core.renderer.Dialect; -import java.util.function.Function; - /** * Supporting class for CypherDSL related customizations. * @@ -33,33 +33,42 @@ import java.util.function.Function; @API(status = API.Status.INTERNAL) public final class SpringDataCypherDsl { - private SpringDataCypherDsl() { - } - + /** + * Will return different delegates for the computing an internal id. + */ public static Function> elementIdOrIdFunction = dialect -> { if (dialect == Dialect.NEO4J_5) { return SpringDataCypherDsl::elementId; - } else if (dialect == Dialect.NEO4J_4) { + } + else if (dialect == Dialect.NEO4J_4) { return SpringDataCypherDsl::id; - } else { + } + else { return named -> { if (named instanceof Node node) { return Cypher.elementId(node); - } else if (named instanceof Relationship relationship) { + } + else if (named instanceof Relationship relationship) { return Cypher.elementId(relationship); - } else { + } + else { throw new IllegalArgumentException("Unsupported CypherDSL type: " + named.getClass()); } }; } }; + private SpringDataCypherDsl() { + } + private static FunctionInvocation id(Named expression) { - return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("id"), expression.getRequiredSymbolicName()); + return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("id"), + expression.getRequiredSymbolicName()); } private static FunctionInvocation elementId(Named expression) { - return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("elementId"), expression.getRequiredSymbolicName()); + return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("elementId"), + expression.getRequiredSymbolicName()); } private static final class ElementIdOrIdFunctionDefinition implements FunctionInvocation.FunctionDefinition { @@ -72,7 +81,7 @@ public final class SpringDataCypherDsl { @Override public String getImplementationName() { - return identifierFunction; + return this.identifierFunction; } @Override @@ -81,4 +90,5 @@ public final class SpringDataCypherDsl { } } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java b/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java index a41857a24..657e7b406 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/UnknownEntityException.java @@ -18,11 +18,12 @@ package org.springframework.data.neo4j.core.mapping; import java.io.Serial; import org.apiguardian.api.API; + import org.springframework.dao.InvalidDataAccessApiUsageException; /** - * Thrown when required information about a class or primary label is requested from the {@link Schema} and that - * information is not available. + * Thrown when required information about a class or primary label is requested from the + * {@link Schema} and that information is not available. * * @author Michael J. Simons * @since 6.0 @@ -32,6 +33,7 @@ public final class UnknownEntityException extends InvalidDataAccessApiUsageExcep @Serial private static final long serialVersionUID = -1769937352513022599L; + private final Class targetClass; public UnknownEntityException(Class targetClass) { @@ -40,6 +42,7 @@ public final class UnknownEntityException extends InvalidDataAccessApiUsageExcep } public Class getTargetClass() { - return targetClass; + return this.targetClass; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AfterConvertCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AfterConvertCallback.java index ee53e9c59..929bd459e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AfterConvertCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AfterConvertCallback.java @@ -15,21 +15,24 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.STABLE; - import org.apiguardian.api.API; import org.neo4j.driver.types.MapAccessor; + import org.springframework.data.mapping.callback.EntityCallback; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import static org.apiguardian.api.API.Status.STABLE; + /** - * A callback that can be used to modify an instance of a {@link Neo4jPersistentEntity} after it has been converted: - * That is, when a Neo4j record has been fully processed and the entity and all its associations have been processed. + * A callback that can be used to modify an instance of a {@link Neo4jPersistentEntity} + * after it has been converted: That is, when a Neo4j record has been fully processed and + * the entity and all its associations have been processed. *

- * There is no reactive variant for this callback. It is safe to use this one for both reactive and imperative workloads. + * There is no reactive variant for this callback. It is safe to use this one for both + * reactive and imperative workloads. * + * @param the type of the entity * @author Michael J. Simons - * @param The type of the entity. * @since 6.3.0 */ @FunctionalInterface @@ -38,11 +41,12 @@ public interface AfterConvertCallback extends EntityCallback { /** * Invoked after converting a Neo4j record (aka after hydrating an entity). - * - * @param instance The instance as hydrated by the {@link org.springframework.data.neo4j.core.mapping.Neo4jEntityConverter}. - * @param entity The entity definition - * @param source The Neo4j record that was used to hydrate the instance. + * @param instance the instance as hydrated by the + * {@link org.springframework.data.neo4j.core.mapping.Neo4jEntityConverter}. + * @param entity the entity definition + * @param source the Neo4j record that was used to hydrate the instance * @return the domain object used further */ T onAfterConvert(T instance, Neo4jPersistentEntity entity, MapAccessor source); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallback.java index 3af23444a..f5cb38535 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallback.java @@ -15,9 +15,8 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.STABLE; - import org.apiguardian.api.API; + import org.springframework.beans.factory.ObjectFactory; import org.springframework.core.Ordered; import org.springframework.data.auditing.AuditingHandler; @@ -25,24 +24,28 @@ import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.mapping.callback.EntityCallback; import org.springframework.util.Assert; +import static org.apiguardian.api.API.Status.STABLE; + /** - * {@link EntityCallback} to populate auditing related fields on an entity about to be bound to a record. + * {@link EntityCallback} to populate auditing related fields on an entity about to be + * bound to a record. * * @author Michael J. Simons - * @soundtrack Iron Maiden - Iron Maiden * @since 6.0.2 */ @API(status = STABLE, since = "6.0.2") public final class AuditingBeforeBindCallback implements BeforeBindCallback, Ordered { + /** + * Public constant for the order in which this callback is applied. + */ public static final int NEO4J_AUDITING_ORDER = 100; private final ObjectFactory auditingHandlerFactory; /** - * Creates a new {@link AuditingBeforeBindCallback} using the given {@link AuditingHandler} provided by the given - * {@link ObjectFactory}. - * + * Creates a new {@link AuditingBeforeBindCallback} using the given + * {@link AuditingHandler} provided by the given {@link ObjectFactory}. * @param auditingHandlerFactory must not be {@literal null}. */ public AuditingBeforeBindCallback(ObjectFactory auditingHandlerFactory) { @@ -51,21 +54,14 @@ public final class AuditingBeforeBindCallback implements BeforeBindCallback the type of the entity * @author Michael J. Simons - * @param The type of the entity. * @since 6.0.2 - * @soundtrack Bon Jovi - Slippery When Wet */ @FunctionalInterface @API(status = STABLE, since = "6.0.2") public interface BeforeBindCallback extends EntityCallback { /** - * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance - * of the domain object. This method is called before converting the {@code entity} to a {@link java.util.Map}, so the - * outcome of this callback is used to create the record for the domain object. - * + * Entity callback method invoked before a domain object is saved. Can return either + * the same or a modified instance of the domain object. This method is called before + * converting the {@code entity} to a {@link java.util.Map}, so the outcome of this + * callback is used to create the record for the domain object. * @param entity the domain object to save. * @return the domain object to be persisted. */ T onBeforeBind(T entity); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java index 07ac938c7..a274d8e7c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/EventSupport.java @@ -15,17 +15,19 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.INTERNAL; - import org.apiguardian.api.API; import org.neo4j.driver.types.MapAccessor; + import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import static org.apiguardian.api.API.Status.INTERNAL; + /** - * Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the framework. + * Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the + * framework. * * @author Michael J. Simons * @since 6.0.2 @@ -33,13 +35,18 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; @API(status = INTERNAL, since = "6.0.2") public final class EventSupport { + private final EntityCallbacks entityCallbacks; + + private EventSupport(EntityCallbacks entityCallbacks) { + this.entityCallbacks = entityCallbacks; + } + /** - * Creates event support containing the required default events plus all entity callbacks discoverable through - * the {@link BeanFactory}. - * - * @param context The mapping context that is used in some of the callbacks. - * @param beanFactory The bean factory used to discover additional callbacks. - * @return A new instance of the event support + * Creates event support containing the required default events plus all entity + * callbacks discoverable through the {@link BeanFactory}. + * @param context the mapping context that is used in some of the callbacks + * @param beanFactory the bean factory used to discover additional callbacks + * @return a new instance of the event support */ public static EventSupport discoverCallbacks(Neo4jMappingContext context, BeanFactory beanFactory) { @@ -49,11 +56,11 @@ public final class EventSupport { } /** - * Creates event support containing the required default events plus all explicitly defined events. - * - * @param context The mapping context that is used in some of the callbacks. + * Creates event support containing the required default events plus all explicitly + * defined events. + * @param context the mapping context that is used in some of the callbacks * @param entityCallbacks predefined callbacks. - * @return A new instance of the event support + * @return a new instance of the event support */ public static EventSupport useExistingCallbacks(Neo4jMappingContext context, EntityCallbacks entityCallbacks) { @@ -67,32 +74,28 @@ public final class EventSupport { entityCallbacks.addEntityCallback(new PostLoadInvocation(context)); } - private final EntityCallbacks entityCallbacks; - - private EventSupport(EntityCallbacks entityCallbacks) { - this.entityCallbacks = entityCallbacks; - } - public T maybeCallBeforeBind(T object) { if (object == null) { return object; } - return entityCallbacks.callback(BeforeBindCallback.class, object); + return this.entityCallbacks.callback(BeforeBindCallback.class, object); } /** - * @param object The freshly converted instance - * @param entity The entity - * @param source The source of the instance - * @param Expected type - * @return The instance to which the callback was applied to + * Will be called after any conversion. + * @param object the freshly converted instance + * @param entity the entity + * @param source the source of the instance + * @param the expected type + * @return the instance to which the callback was applied to */ public T maybeCallAfterConvert(T object, Neo4jPersistentEntity entity, MapAccessor source) { if (object == null) { return object; } - return entityCallbacks.callback(AfterConvertCallback.class, object, entity, source); + return this.entityCallbacks.callback(AfterConvertCallback.class, object, entity, source); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdGeneratingBeforeBindCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdGeneratingBeforeBindCallback.java index bed62c253..14824e123 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdGeneratingBeforeBindCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdGeneratingBeforeBindCallback.java @@ -22,7 +22,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; * Callback used to call the ID generator configured for an entity just before binding. * * @author Michael J. Simons - * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack) * @since 6.0.2 */ final class IdGeneratingBeforeBindCallback implements BeforeBindCallback, Ordered { @@ -35,11 +34,12 @@ final class IdGeneratingBeforeBindCallback implements BeforeBindCallback @Override public Object onBeforeBind(Object entity) { - return idPopulator.populateIfNecessary(entity); + return this.idPopulator.populateIfNecessary(entity); } @Override public int getOrder() { return AuditingBeforeBindCallback.NEO4J_AUDITING_ORDER + 10; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulator.java index ba3089518..1c8498a9b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulator.java @@ -18,10 +18,10 @@ package org.springframework.data.neo4j.core.mapping.callback; import java.util.Optional; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.neo4j.core.mapping.IdDescription; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.core.mapping.IdDescription; import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.util.Assert; @@ -45,15 +45,17 @@ final class IdPopulator { Assert.notNull(entity, "Entity may not be null"); - Neo4jPersistentEntity nodeDescription = neo4jMappingContext.getRequiredPersistentEntity(entity.getClass()); + Neo4jPersistentEntity nodeDescription = this.neo4jMappingContext + .getRequiredPersistentEntity(entity.getClass()); IdDescription idDescription = nodeDescription.getIdDescription(); if (idDescription == null) { if (nodeDescription.isRelationshipPropertiesEntity()) { return entity; - } else { - throw new IllegalStateException( - "Cannot persist implicit entity due to missing id property on " + nodeDescription.getUnderlyingClass()); + } + else { + throw new IllegalStateException("Cannot persist implicit entity due to missing id property on " + + nodeDescription.getUnderlyingClass()); } } @@ -77,15 +79,19 @@ final class IdPopulator { Optional optionalIdGeneratorRef = idDescription.getIdGeneratorRef(); if (optionalIdGeneratorRef.isPresent()) { - idGenerator = neo4jMappingContext.getIdGenerator(optionalIdGeneratorRef.get()).orElseThrow( - () -> new IllegalStateException("Id generator named " + optionalIdGeneratorRef.get() + " not found")); - } else { + idGenerator = this.neo4jMappingContext.getIdGenerator(optionalIdGeneratorRef.get()) + .orElseThrow(() -> new IllegalStateException( + "Id generator named " + optionalIdGeneratorRef.get() + " not found")); + } + else { - idGenerator = neo4jMappingContext.getOrCreateIdGeneratorOfType(idDescription.getIdGeneratorClass().orElseThrow( - () -> new IllegalStateException("Neither generator reference nor generator class configured"))); + idGenerator = this.neo4jMappingContext.getOrCreateIdGeneratorOfType(idDescription.getIdGeneratorClass() + .orElseThrow( + () -> new IllegalStateException("Neither generator reference nor generator class configured"))); } propertyAccessor.setProperty(idProperty, idGenerator.generateId(nodeDescription.getPrimaryLabel(), entity)); return propertyAccessor.getBean(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/PostLoadInvocation.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/PostLoadInvocation.java index 1cf89c684..28e3ac1bf 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/PostLoadInvocation.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/PostLoadInvocation.java @@ -16,13 +16,14 @@ package org.springframework.data.neo4j.core.mapping.callback; import org.neo4j.driver.types.MapAccessor; + import org.springframework.core.Ordered; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; /** - * Triggers {@link Neo4jMappingContext#invokePostLoad(Neo4jPersistentEntity, Object)} via the {@link AfterConvertCallback} - * mechanism. + * Triggers {@link Neo4jMappingContext#invokePostLoad(Neo4jPersistentEntity, Object)} via + * the {@link AfterConvertCallback} mechanism. * * @author Michael J. Simons */ @@ -42,6 +43,7 @@ final class PostLoadInvocation implements AfterConvertCallback, Ordered @Override public Object onAfterConvert(Object instance, Neo4jPersistentEntity entity, MapAccessor source) { - return neo4jMappingContext.invokePostLoad(entity, instance); + return this.neo4jMappingContext.invokePostLoad(entity, instance); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallback.java index 51d4d457e..e6da002aa 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallback.java @@ -15,10 +15,9 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.STABLE; - import org.apiguardian.api.API; import org.reactivestreams.Publisher; + import org.springframework.beans.factory.ObjectFactory; import org.springframework.core.Ordered; import org.springframework.data.auditing.AuditingHandler; @@ -26,24 +25,28 @@ import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; import org.springframework.data.mapping.callback.EntityCallback; import org.springframework.util.Assert; +import static org.apiguardian.api.API.Status.STABLE; + /** - * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be bound to a record. + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about + * to be bound to a record. * * @author Michael J. Simons - * @soundtrack Iron Maiden - The Number Of The Beast * @since 6.0.2 */ @API(status = STABLE, since = "6.0.2") public final class ReactiveAuditingBeforeBindCallback implements ReactiveBeforeBindCallback, Ordered { + /** + * Public constant for the order in which this callback is applied. + */ public static final int NEO4J_REACTIVE_AUDITING_ORDER = 100; private final ObjectFactory auditingHandlerFactory; /** - * Creates a new {@link ReactiveAuditingBeforeBindCallback} using the {@link AuditingHandler} provided by the given - * {@link ObjectFactory}. - * + * Creates a new {@link ReactiveAuditingBeforeBindCallback} using the + * {@link AuditingHandler} provided by the given {@link ObjectFactory}. * @param auditingHandlerFactory must not be {@literal null}. */ public ReactiveAuditingBeforeBindCallback(ObjectFactory auditingHandlerFactory) { @@ -52,22 +55,15 @@ public final class ReactiveAuditingBeforeBindCallback implements ReactiveBeforeB this.auditingHandlerFactory = auditingHandlerFactory; } - /* - * (non-Javadoc) - * @see org.springframework.data.neo4j.repository.event.ReactiveBeforeBindCallback#onBeforeBind(java.lang.Object) - */ @Override public Publisher onBeforeBind(Object entity) { - return auditingHandlerFactory.getObject().markAudited(entity); + return this.auditingHandlerFactory.getObject().markAudited(entity); } - /* - * (non-Javadoc) - * @see org.springframework.core.Ordered#getOrder() - */ @Override public int getOrder() { return NEO4J_REACTIVE_AUDITING_ORDER; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveBeforeBindCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveBeforeBindCallback.java index 801ec80fd..cd1041722 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveBeforeBindCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveBeforeBindCallback.java @@ -15,34 +15,35 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.STABLE; - import org.apiguardian.api.API; import org.reactivestreams.Publisher; + import org.springframework.data.mapping.callback.EntityCallback; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import static org.apiguardian.api.API.Status.STABLE; + /** - * Entity callback triggered before an Entity is bound to a record (represented by a {@link java.util.Map - * java.util.Map<String, Object>}). + * Entity callback triggered before an Entity is bound to a record (represented by a + * {@link java.util.Map java.util.Map<String, Object>}). * + * @param the type of the entity. * @author Michael J. Simons - * @param The type of the entity. - * @soundtrack Iron Maiden - Killers - * @see ReactiveEntityCallbacks * @since 6.0.2 + * @see ReactiveEntityCallbacks */ @FunctionalInterface @API(status = STABLE, since = "6.0.2") public interface ReactiveBeforeBindCallback extends EntityCallback { /** - * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance - * of the domain object. This method is called before converting the {@code entity} to a {@link java.util.Map}, so the - * outcome of this callback is used to create the record for the domain object. - * + * Entity callback method invoked before a domain object is saved. Can return either + * the same or a modified instance of the domain object. This method is called before + * converting the {@code entity} to a {@link java.util.Map}, so the outcome of this + * callback is used to create the record for the domain object. * @param entity the domain object to save. * @return the domain object to be persisted. */ Publisher onBeforeBind(T entity); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveEventSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveEventSupport.java index fc4d104e3..5f1a96986 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveEventSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveEventSupport.java @@ -15,18 +15,19 @@ */ package org.springframework.data.neo4j.core.mapping.callback; -import static org.apiguardian.api.API.Status.INTERNAL; - +import org.apiguardian.api.API; import reactor.core.publisher.Mono; -import org.apiguardian.api.API; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import static org.apiguardian.api.API.Status.INTERNAL; + /** - * Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the framework. + * Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the + * framework. * * @author Michael J. Simons * @since 6.0.2 @@ -34,13 +35,18 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @API(status = INTERNAL, since = "6.0.2") public final class ReactiveEventSupport { + private final ReactiveEntityCallbacks entityCallbacks; + + private ReactiveEventSupport(ReactiveEntityCallbacks entityCallbacks) { + this.entityCallbacks = entityCallbacks; + } + /** - * Creates event support containing the required default events plus all entity callbacks discoverable through - * the {@link BeanFactory}. - * - * @param context The mapping context that is used in some of the callbacks. - * @param beanFactory The bean factory used to discover additional callbacks. - * @return A new instance of the event support + * Creates event support containing the required default events plus all entity + * callbacks discoverable through the {@link BeanFactory}. + * @param context the mapping context that is used in some of the callbacks. + * @param beanFactory the bean factory used to discover additional callbacks. + * @return a new instance of the event support */ public static ReactiveEventSupport discoverCallbacks(Neo4jMappingContext context, BeanFactory beanFactory) { @@ -50,13 +56,14 @@ public final class ReactiveEventSupport { } /** - * Creates event support containing the required default events plus all explicitly defined events. - * - * @param context The mapping context that is used in some of the callbacks. + * Creates event support containing the required default events plus all explicitly + * defined events. + * @param context the mapping context that is used in some of the callbacks. * @param entityCallbacks predefined callbacks. - * @return A new instance of the event support + * @return a new instance of the event support */ - public static ReactiveEventSupport useExistingCallbacks(Neo4jMappingContext context, ReactiveEntityCallbacks entityCallbacks) { + public static ReactiveEventSupport useExistingCallbacks(Neo4jMappingContext context, + ReactiveEntityCallbacks entityCallbacks) { addDefaultEntityCallbacks(context, entityCallbacks); return new ReactiveEventSupport(entityCallbacks); @@ -69,17 +76,12 @@ public final class ReactiveEventSupport { entityCallbacks.addEntityCallback(new PostLoadInvocation(context)); } - private final ReactiveEntityCallbacks entityCallbacks; - - private ReactiveEventSupport(ReactiveEntityCallbacks entityCallbacks) { - this.entityCallbacks = entityCallbacks; - } - public Mono maybeCallBeforeBind(T object) { if (object == null) { return Mono.empty(); } - return entityCallbacks.callback(ReactiveBeforeBindCallback.class, object); + return this.entityCallbacks.callback(ReactiveBeforeBindCallback.class, object); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveIdGeneratingBeforeBindCallback.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveIdGeneratingBeforeBindCallback.java index 2a0bdb1be..5a37b537b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveIdGeneratingBeforeBindCallback.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveIdGeneratingBeforeBindCallback.java @@ -15,9 +15,9 @@ */ package org.springframework.data.neo4j.core.mapping.callback; +import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import org.reactivestreams.Publisher; import org.springframework.core.Ordered; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -25,7 +25,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; * Callback used to call the ID generator configured for an entity just before binding. * * @author Michael J. Simons - * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack) * @since 6.0.2 */ final class ReactiveIdGeneratingBeforeBindCallback implements ReactiveBeforeBindCallback, Ordered { @@ -39,11 +38,12 @@ final class ReactiveIdGeneratingBeforeBindCallback implements ReactiveBeforeBind @Override public Publisher onBeforeBind(Object entity) { - return Mono.fromSupplier(() -> idPopulator.populateIfNecessary(entity)); + return Mono.fromSupplier(() -> this.idPopulator.populateIfNecessary(entity)); } @Override public int getOrder() { return ReactiveAuditingBeforeBindCallback.NEO4J_REACTIVE_AUDITING_ORDER + 10; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java index 1d787963f..1f43aef99 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/callback/package-info.java @@ -1,18 +1,36 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - This package contains the callback API. There are both imperative and reactive callbacks available that get invoked - just before an entity is bound to a statement. These can be implemented by client code. For further convenience, - both imperative and reactive auditing callbacks are available. The config package contains a registrar and an - annotation to enable those without having to provide the beans manually. - - The event system comes in two flavours: Events that are based on Spring's application event system and callbacks that - are based on Spring Data's callback system. Application events can be configured to run asynchronously, which make - them a bad fit in transactional workloads. - - As a rule of thumb, use Entity callbacks for modifying entities before persisting and application events otherwise. - The best option however to react in a transactional way to changes of an entity is to implement - {@link org.springframework.data.domain.DomainEvents} on an aggregate root. - * + * This package contains the callback API. There are both + * imperative and reactive callbacks available that get invoked just before an entity is + * bound to a statement. These can be implemented by client code. For further convenience, + * both imperative and reactive auditing callbacks are available. The config package + * contains a registrar and an annotation to enable those without having to provide the + * beans manually. + * + * The event system comes in two flavours: Events that are based on Spring's application + * event system and callbacks that are based on Spring Data's callback system. Application + * events can be configured to run asynchronously, which make them a bad fit in + * transactional workloads. + * + * As a rule of thumb, use Entity callbacks for modifying entities before persisting and + * application events otherwise. The best option however to react in a transactional way + * to changes of an entity is to implement + * {@link org.springframework.data.domain.DomainEvents} on an aggregate root. * @author Michael J. Simons */ package org.springframework.data.neo4j.core.mapping.callback; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java b/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java index 9835da9ce..1dbff13fa 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java @@ -1,9 +1,24 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - The main mapping framework. This package orchestrates the reading and writing of entities and all tasks related to it. - The only public API of this package is the subpackage {@literal callback}, containing the event support. - The core package itself has to be considered an internal api, and we don't give any guarantees of API stability. - * + * The main mapping framework. This package orchestrates the + * reading and writing of entities and all tasks related to it. The only public API of + * this package is the subpackage {@literal callback}, containing the event support. The + * core package itself has to be considered an internal api, and we don't give any + * guarantees of API stability. * @author Michael J. Simons */ @NullMarked diff --git a/src/main/java/org/springframework/data/neo4j/core/package-info.java b/src/main/java/org/springframework/data/neo4j/core/package-info.java index 09f7b4887..33a982279 100644 --- a/src/main/java/org/springframework/data/neo4j/core/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/package-info.java @@ -1,9 +1,23 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - This package contains the core infrastructure for creating an imperative or reactive client that can execute queries. - Packages marked as `@API(status = API.Status.STABLE)` are safe to be used. The core package provides access to both - the imperative and reactive variants of the client and the template. - * + * This package contains the core infrastructure for creating an + * imperative or reactive client that can execute queries. Packages marked as `@API(status + * = API.Status.STABLE)` are safe to be used. The core package provides access to both the + * imperative and reactive variants of the client and the template. */ @NullMarked package org.springframework.data.neo4j.core; diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java b/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java index cde5bec5e..c48702cee 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java @@ -20,60 +20,52 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.function.UnaryOperator; -import java.util.stream.Collectors; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; -import org.neo4j.driver.Values; -import org.neo4j.driver.types.TypeSystem; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.GenericTypeResolver; + import org.springframework.core.annotation.AliasFor; import org.springframework.data.neo4j.core.convert.ConvertWith; import org.springframework.data.neo4j.core.convert.Neo4jConversionService; -import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; -import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.core.schema.CompositeProperty.Phase; import org.springframework.data.util.TypeInformation; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** - * This annotation indicates a {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty persistent property} - * that is composed of multiple properties on a node or relationship. The properties must share a common prefix. SDN defaults - * to the name of the field declared on the {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity persistent entity}. - *

This annotation is mainly to be used on properties of type {@link Map Map<String, Object>}. All values in the - * map are subject to conversions by other registered converters. Nested maps are not supported. - *

This annotation is the pendant to Neo4j-OGMs {@literal org.neo4j.ogm.annotation.Properties}. + * This annotation indicates a + * {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty persistent + * property} that is composed of multiple properties on a node or relationship. The + * properties must share a common prefix. SDN defaults to the name of the field declared + * on the {@link org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity + * persistent entity}. + *

+ * This annotation is mainly to be used on properties of type {@link Map Map<String, + * Object>}. All values in the map are subject to conversions by other registered + * converters. Nested maps are not supported. + *

+ * This annotation is the pendant to Neo4j-OGMs + * {@literal org.neo4j.ogm.annotation.Properties}. * * @author Michael J. Simons - * @soundtrack Slime - Viva la Muerte * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD}) +@Target({ ElementType.FIELD }) @Inherited @ConvertWith(converterFactory = CompositePropertyConverterFactory.class) @API(status = API.Status.STABLE, since = "6.0") public @interface CompositeProperty { /** - * @return A converter that allows to store arbitrary objects as decomposed maps on nodes and relationships. The - * default converter allows only maps as composite properties. + * Defines a dedicated converter for this field. + * @return A converter that allows to store arbitrary objects as decomposed maps on + * nodes and relationships. The default converter allows only maps as composite + * properties. */ @AliasFor(annotation = ConvertWith.class, value = "converter") Class converter() default CompositeProperty.DefaultToMapConverter.class; @@ -82,29 +74,45 @@ public @interface CompositeProperty { String converterRef() default ""; /** - * Allows to specify the prefix for the map properties. The default empty value instructs SDN to use the - * field name of the annotated property. - * - * @return The prefix used for storing the properties in the graph on the node or relationship + * Allows to specify the prefix for the map properties. The default empty value + * instructs SDN to use the field name of the annotated property. + * @return The prefix used for storing the properties in the graph on the node or + * relationship */ String prefix() default ""; /** - * Allows to specify the delimiter between prefix and map value on the properties of the node or relationship in the - * graph. Defaults to {@literal .}. - * + * Allows to specify the delimiter between prefix and map value on the properties of + * the node or relationship in the graph. Defaults to {@literal .}. * @return Delimiter to use in the stored property names */ String delimiter() default "."; /** - * This attribute allows for configuring a transformation that is applied to the maps keys. {@link Phase#WRITE} is applied - * before writing the map, {@link Phase#READ} is applied on write. - * + * This attribute allows for configuring a transformation that is applied to the maps + * keys. {@link Phase#WRITE} is applied before writing the map, {@link Phase#READ} is + * applied on write. * @return A transformation to be used on enum keys. */ Class> transformKeysWith() default NoopTransformation.class; + /** + * Phase of the mapping currently taking place. + */ + enum Phase { + + /** + * Writing to the graph. + */ + WRITE, + + /** + * Graph properties are mapped to key/values of a map contained in an entity. + */ + READ + + } + /** * The default operation for transforming the keys. Defaults to a no-op. */ @@ -114,19 +122,21 @@ public @interface CompositeProperty { public String apply(Phase phase, String s) { return s; } + } /** - * The default implementation, passing map properties through as they are on the way to the graph and possibly - * applying a post processor on the way out of the graph. + * The default implementation, passing map properties through as they are on the way + * to the graph and possibly applying a post processor on the way out of the graph. * - * @param The type of the keys. + * @param the type of the keys */ final class DefaultToMapConverter implements Neo4jPersistentPropertyToMapConverter> { /** - * A post processor of the map that is eventually be stored in the entity. In case a user wishes for entities - * with immutable collection, that would be the place to configure it. + * A post processor of the map that is eventually be stored in the entity. In case + * a user wishes for entities with immutable collection, that would be the place + * to configure it. */ private final UnaryOperator> mapPostProcessor = UnaryOperator.identity(); @@ -145,219 +155,18 @@ public @interface CompositeProperty { Map decomposed = new HashMap<>(property.size()); property.forEach( - (k, v) -> decomposed.put(k, conversionService.writeValue(v, typeInformationForValues, null))); + (k, v) -> decomposed.put(k, conversionService.writeValue(v, this.typeInformationForValues, null))); return decomposed; } @Override public Map compose(Map source, Neo4jConversionService conversionService) { Map composed = new HashMap<>(source.size()); - source.forEach((k, v) -> composed.put(k, conversionService.readValue(v, typeInformationForValues, null))); - return mapPostProcessor.apply(composed); + source.forEach( + (k, v) -> composed.put(k, conversionService.readValue(v, this.typeInformationForValues, null))); + return this.mapPostProcessor.apply(composed); } + } - /** - * Phase of the mapping currently taking place. - */ - enum Phase { - /** - * Writing to the graph. - */ - WRITE, - - /** - * Graph properties are mapped to key/values of a map contained in an entity. - */ - READ - } -} - -/** - * Dedicated and highly specialized converter for reading and writing {@link Map} with either enum or string keys - * into multiple properties of Nodes or Relationships inside the Neo4j database. This is an internal API only. - * - * @param The type of the key - */ -final class CompositePropertyConverter implements Neo4jPersistentPropertyConverter

{ - - private final Neo4jPersistentPropertyToMapConverter delegate; - - private final String prefixWithDelimiter; - - private final Neo4jConversionService neo4jConversionService; - - private final Class typeOfKeys; - - private final Function keyWriter; - - private final Function keyReader; - - CompositePropertyConverter( - Neo4jPersistentPropertyToMapConverter delegate, - String prefixWithDelimiter, - Neo4jConversionService neo4jConversionService, - Class typeOfKeys, - Function keyWriter, - Function keyReader - ) { - this.delegate = delegate; - this.prefixWithDelimiter = prefixWithDelimiter; - this.neo4jConversionService = neo4jConversionService; - this.typeOfKeys = typeOfKeys; - this.keyWriter = keyWriter; - this.keyReader = keyReader; - } - - @Override - public Value write(@Nullable P property) { - - Map source = delegate.decompose(property, neo4jConversionService); - Map temp = new HashMap<>(); - source.forEach((key, value) -> temp.put(prefixWithDelimiter + keyWriter.apply(key), value)); - return Values.value(temp); - } - - @Override - @Nullable - public P read(@Nullable Value source) { - - if (source == null || TypeSystem.getDefault().NULL().isTypeOf(source)) { - return null; - } - - Map temp = new HashMap<>(); - source.keys().forEach(k -> { - if (k.startsWith(prefixWithDelimiter)) { - K key = keyReader.apply(k.substring(prefixWithDelimiter.length())); - temp.put(key, source.get(k)); - } - }); - return this.delegate.compose(temp, neo4jConversionService); - } - - /** - * Internally used via reflection. - * - * @return The type of the underlying delegate. - */ - @SuppressWarnings("unused") - Class getClassOfDelegate() { - return this.delegate.getClass(); - } -} - -/** - * Internal API for creating composite converters. - */ -final class CompositePropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory { - - private static final String KEY_TYPE_KEY = "K"; - private static final String PROPERTY_TYPE_KEY = "P"; - - private final BeanFactory beanFactory; - private final Neo4jConversionService conversionServiceDelegate; - - CompositePropertyConverterFactory(BeanFactory beanFactory, Neo4jConversionService conversionServiceDelegate) { - this.beanFactory = beanFactory; - this.conversionServiceDelegate = conversionServiceDelegate; - } - - @SuppressWarnings({"raw", "unchecked"}) // Due to dynamic enum retrieval - @Override - public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) { - - CompositeProperty config = persistentProperty.getRequiredAnnotation(CompositeProperty.class); - Class delegateClass = config.converter(); - Neo4jPersistentPropertyToMapConverter> delegate = null; - - if (StringUtils.hasText(config.converterRef())) { - if (beanFactory == null) { - throw new IllegalStateException( - "The default composite converter factory has been configured without a bean factory and cannot use a converter from the application context"); - } - - delegate = beanFactory.getBean(config.converterRef(), Neo4jPersistentPropertyToMapConverter.class); - delegateClass = delegate.getClass(); - } - - Class componentType; - - if (persistentProperty.isMap()) { - componentType = persistentProperty.getComponentType(); - } else { - - if (delegateClass == CompositeProperty.DefaultToMapConverter.class) { - throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName() - + " can only be used on Map properties without additional configuration. Was " - + generateLocation( - persistentProperty)); - } - - // Avoid resolving this as long as possible. - Map typeVariableMap = GenericTypeResolver.getTypeVariableMap(delegateClass).entrySet() - .stream() - .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)); - - Assert.isTrue(typeVariableMap.containsKey(KEY_TYPE_KEY), - () -> "SDN could not determine the key type of your toMap converter " + generateLocation( - persistentProperty)); - Assert.isTrue(typeVariableMap.containsKey(PROPERTY_TYPE_KEY), - () -> "SDN could not determine the property type of your toMap converter " + generateLocation( - persistentProperty)); - - Type type = typeVariableMap.get(PROPERTY_TYPE_KEY); - if (persistentProperty.isCollectionLike() && type instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) type; - if (persistentProperty.getType().equals(pt.getRawType()) && pt.getActualTypeArguments().length == 1) { - type = ((ParameterizedType) type).getActualTypeArguments()[0]; - } - } - - if (persistentProperty.getActualType() != type) { - var typeName = Optional.ofNullable(type).map(Type::getTypeName).orElse("n/a"); - throw new IllegalArgumentException( - "The property type `" + typeName + "` created by `" - + delegateClass.getName() + "` " + generateLocation(persistentProperty) - + " doesn't match the actual property type"); - } - componentType = (Class) typeVariableMap.get(KEY_TYPE_KEY); - } - - boolean isEnum = componentType != null && componentType.isEnum(); - if (!(componentType == String.class || isEnum)) { - throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName() - + " can only be used on Map properties with a key type of String or enum. Was " + generateLocation( - persistentProperty)); - } - - BiFunction keyTransformation = BeanUtils.instantiateClass(config.transformKeysWith()); - - Function keyReader; - Function keyWriter; - if (isEnum) { - keyReader = key -> Enum.valueOf(((Class) componentType), keyTransformation.apply(Phase.READ, key)); - keyWriter = (Enum key) -> keyTransformation.apply(Phase.WRITE, key.name()); - } else { - keyReader = key -> keyTransformation.apply(Phase.READ, key); - keyWriter = (String key) -> keyTransformation.apply(Phase.WRITE, key); - } - - if (delegate == null) { - if (delegateClass == CompositeProperty.DefaultToMapConverter.class) { - delegate = new CompositeProperty.DefaultToMapConverter(TypeInformation.of(persistentProperty.getActualType())); - } else { - delegate = BeanUtils.instantiateClass(delegateClass); - } - } - - String prefixWithDelimiter = persistentProperty.computePrefixWithDelimiter(); - return new CompositePropertyConverter( - delegate, prefixWithDelimiter, conversionServiceDelegate, Objects.requireNonNull(componentType), keyWriter, keyReader); - } - - private static String generateLocation(Neo4jPersistentProperty persistentProperty) { - return "used on `" + persistentProperty.getFieldName() + "` in `" + persistentProperty.getOwner().getName() - + "`"; - } } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverter.java b/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverter.java new file mode 100644 index 000000000..14efdec15 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverter.java @@ -0,0 +1,100 @@ +/* + * Copyright 2011-2025 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.core.schema; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; +import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.TypeSystem; + +import org.springframework.data.neo4j.core.convert.Neo4jConversionService; +import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; +import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; + +/** + * Dedicated and highly specialized converter for reading and writing {@link Map} with + * either enum or string keys into multiple properties of Nodes or Relationships inside + * the Neo4j database. This is an internal API only. + * + * @param the type of the key + * @param

the type of the property value + * @author Michael J. Simons + */ +final class CompositePropertyConverter implements Neo4jPersistentPropertyConverter

{ + + private final Neo4jPersistentPropertyToMapConverter delegate; + + private final String prefixWithDelimiter; + + private final Neo4jConversionService neo4jConversionService; + + private final Class typeOfKeys; + + private final Function keyWriter; + + private final Function keyReader; + + CompositePropertyConverter(Neo4jPersistentPropertyToMapConverter delegate, String prefixWithDelimiter, + Neo4jConversionService neo4jConversionService, Class typeOfKeys, Function keyWriter, + Function keyReader) { + this.delegate = delegate; + this.prefixWithDelimiter = prefixWithDelimiter; + this.neo4jConversionService = neo4jConversionService; + this.typeOfKeys = typeOfKeys; + this.keyWriter = keyWriter; + this.keyReader = keyReader; + } + + @Override + public Value write(@Nullable P property) { + + Map source = this.delegate.decompose(property, this.neo4jConversionService); + Map temp = new HashMap<>(); + source.forEach((key, value) -> temp.put(this.prefixWithDelimiter + this.keyWriter.apply(key), value)); + return Values.value(temp); + } + + @Override + @Nullable public P read(@Nullable Value source) { + + if (source == null || TypeSystem.getDefault().NULL().isTypeOf(source)) { + return null; + } + + Map temp = new HashMap<>(); + source.keys().forEach(k -> { + if (k.startsWith(this.prefixWithDelimiter)) { + K key = this.keyReader.apply(k.substring(this.prefixWithDelimiter.length())); + temp.put(key, source.get(k)); + } + }); + return this.delegate.compose(temp, this.neo4jConversionService); + } + + /** + * Internally used via reflection. + * @return the type of the underlying delegate. + */ + @SuppressWarnings("unused") + Class getClassOfDelegate() { + return this.delegate.getClass(); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverterFactory.java b/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverterFactory.java new file mode 100644 index 000000000..0fd9cdfe2 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/schema/CompositePropertyConverterFactory.java @@ -0,0 +1,162 @@ +/* + * Copyright 2011-2025 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.core.schema; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.GenericTypeResolver; +import org.springframework.data.neo4j.core.convert.Neo4jConversionService; +import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; +import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory; +import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Internal API for creating composite converters. + * + * @author Michael J. Simons + */ +final class CompositePropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory { + + private static final String KEY_TYPE_KEY = "K"; + + private static final String PROPERTY_TYPE_KEY = "P"; + + private final BeanFactory beanFactory; + + private final Neo4jConversionService conversionServiceDelegate; + + CompositePropertyConverterFactory(BeanFactory beanFactory, Neo4jConversionService conversionServiceDelegate) { + this.beanFactory = beanFactory; + this.conversionServiceDelegate = conversionServiceDelegate; + } + + private static String generateLocation(Neo4jPersistentProperty persistentProperty) { + return "used on `" + persistentProperty.getFieldName() + "` in `" + persistentProperty.getOwner().getName() + + "`"; + } + + @SuppressWarnings({ "raw", "unchecked" }) // Due to dynamic enum retrieval + @Override + public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) { + + CompositeProperty config = persistentProperty.getRequiredAnnotation(CompositeProperty.class); + Class delegateClass = config.converter(); + Neo4jPersistentPropertyToMapConverter> delegate = null; + + if (StringUtils.hasText(config.converterRef())) { + if (this.beanFactory == null) { + throw new IllegalStateException( + "The default composite converter factory has been configured without a bean factory and cannot use a converter from the application context"); + } + + delegate = this.beanFactory.getBean(config.converterRef(), Neo4jPersistentPropertyToMapConverter.class); + delegateClass = delegate.getClass(); + } + + Class componentType; + + if (persistentProperty.isMap()) { + componentType = persistentProperty.getComponentType(); + } + else { + + if (delegateClass == CompositeProperty.DefaultToMapConverter.class) { + throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName() + + " can only be used on Map properties without additional configuration. Was " + + generateLocation(persistentProperty)); + } + + // Avoid resolving this as long as possible. + Map typeVariableMap = GenericTypeResolver.getTypeVariableMap(delegateClass) + .entrySet() + .stream() + .collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)); + + Assert.isTrue(typeVariableMap.containsKey(KEY_TYPE_KEY), + () -> "SDN could not determine the key type of your toMap converter " + + generateLocation(persistentProperty)); + Assert.isTrue(typeVariableMap.containsKey(PROPERTY_TYPE_KEY), + () -> "SDN could not determine the property type of your toMap converter " + + generateLocation(persistentProperty)); + + Type type = typeVariableMap.get(PROPERTY_TYPE_KEY); + if (persistentProperty.isCollectionLike() && type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + if (persistentProperty.getType().equals(pt.getRawType()) && pt.getActualTypeArguments().length == 1) { + type = ((ParameterizedType) type).getActualTypeArguments()[0]; + } + } + + if (persistentProperty.getActualType() != type) { + var typeName = Optional.ofNullable(type).map(Type::getTypeName).orElse("n/a"); + throw new IllegalArgumentException( + "The property type `" + typeName + "` created by `" + delegateClass.getName() + "` " + + generateLocation(persistentProperty) + " doesn't match the actual property type"); + } + componentType = (Class) typeVariableMap.get(KEY_TYPE_KEY); + } + + boolean isEnum = componentType != null && componentType.isEnum(); + if (!(componentType == String.class || isEnum)) { + throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName() + + " can only be used on Map properties with a key type of String or enum. Was " + + generateLocation(persistentProperty)); + } + + BiFunction keyTransformation = BeanUtils + .instantiateClass(config.transformKeysWith()); + + Function keyReader; + Function keyWriter; + if (isEnum) { + keyReader = key -> Enum.valueOf(((Class) componentType), + keyTransformation.apply(CompositeProperty.Phase.READ, key)); + keyWriter = (Enum key) -> keyTransformation.apply(CompositeProperty.Phase.WRITE, key.name()); + } + else { + keyReader = key -> keyTransformation.apply(CompositeProperty.Phase.READ, key); + keyWriter = (String key) -> keyTransformation.apply(CompositeProperty.Phase.WRITE, key); + } + + if (delegate == null) { + if (delegateClass == CompositeProperty.DefaultToMapConverter.class) { + delegate = new CompositeProperty.DefaultToMapConverter( + TypeInformation.of(persistentProperty.getActualType())); + } + else { + delegate = BeanUtils.instantiateClass(delegateClass); + } + } + + String prefixWithDelimiter = persistentProperty.computePrefixWithDelimiter(); + return new CompositePropertyConverter(delegate, prefixWithDelimiter, this.conversionServiceDelegate, + Objects.requireNonNull(componentType), keyWriter, keyReader); + } + +} diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/DynamicLabels.java b/src/main/java/org/springframework/data/neo4j/core/schema/DynamicLabels.java index c25eb75f9..3ca603974 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/DynamicLabels.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/DynamicLabels.java @@ -24,16 +24,17 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; /** - * This annotation can be used on a field of type {@link java.util.Collection Collection<String>}. The content of - * this field will be treated as dynamic or runtime managed labels. This means: All labels that are not statically - * defined via the class hierarchy and the corresponding {@link Node @Node} annotation are added to this list while - * loading the entity and all values contained in the collection will be added to the nodes labels. + * This annotation can be used on a field of type {@link java.util.Collection + * Collection<String>}. The content of this field will be treated as dynamic or + * runtime managed labels. This means: All labels that are not statically defined via the + * class hierarchy and the corresponding {@link Node @Node} annotation are added to this + * list while loading the entity and all values contained in the collection will be added + * to the nodes labels. *

- * Labels not defined through the class hierarchy or the list of dynamic labels will be removed from the database when - * {@link DynamicLabels @DynamicLabels} is used. + * Labels not defined through the class hierarchy or the list of dynamic labels will be + * removed from the database when {@link DynamicLabels @DynamicLabels} is used. * * @author Michael J. Simons - * @soundtrack Danger Dan - Nudeln und Klopapier * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) @@ -41,4 +42,5 @@ import org.apiguardian.api.API; @Documented @API(status = API.Status.STABLE, since = "6.0") public @interface DynamicLabels { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/GeneratedValue.java b/src/main/java/org/springframework/data/neo4j/core/schema/GeneratedValue.java index 4c8f79fc3..b3151ea7f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/GeneratedValue.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/GeneratedValue.java @@ -24,14 +24,15 @@ import java.lang.annotation.Target; import java.util.UUID; import org.apiguardian.api.API; + import org.springframework.core.annotation.AliasFor; /** - * Indicates a generated id. Ids can be generated internally. by the database itself or by an external generator. This - * annotation defaults to the internally generated ids. + * Indicates a generated id. Ids can be generated internally. by the database itself or by + * an external generator. This annotation defaults to the internally generated ids. *

- * An internal id has no corresponding property on a node. It can only be retrieved via the built-in Cypher function - * {@code id()}. + * An internal id has no corresponding property on a node. It can only be retrieved via + * the built-in Cypher function {@code id()}. *

* To use an external id generator, specify on the * @@ -39,13 +40,14 @@ import org.springframework.core.annotation.AliasFor; * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Documented @Inherited @API(status = API.Status.STABLE, since = "6.0") public @interface GeneratedValue { /** + * Configures the ID generator to use. * @return The generator to use. * @see #generatorClass() */ @@ -53,18 +55,22 @@ public @interface GeneratedValue { Class> value() default GeneratedValue.InternalIdGenerator.class; /** - * @return The generator to use. Defaults to {@link InternalIdGenerator}, which indicates database generated values. + * Configures the ID generator to use. + * @return The generator to use. Defaults to {@link InternalIdGenerator}, which + * indicates database generated values. */ @AliasFor("value") Class> generatorClass() default GeneratedValue.InternalIdGenerator.class; /** + * Configures a bean reference to a bean used as ID generator. * @return An optional reference to a bean to be used as ID generator. */ String generatorRef() default ""; /** - * This {@link IdGenerator} does nothing. It is used for relying on the internal, database-side created id. + * This {@link IdGenerator} does nothing. It is used for relying on the internal, + * database-side created id. */ final class InternalIdGenerator implements IdGenerator { @@ -72,11 +78,12 @@ public @interface GeneratedValue { public Void generateId(String primaryLabel, Object entity) { return null; } + } /** - * This generator is automatically applied when a field of type {@link java.util.UUID} is annotated with - * {@link Id @Id} and {@link GeneratedValue @GeneratedValue}. + * This generator is automatically applied when a field of type {@link java.util.UUID} + * is annotated with {@link Id @Id} and {@link GeneratedValue @GeneratedValue}. * */ final class UUIDGenerator implements IdGenerator { @@ -85,5 +92,7 @@ public @interface GeneratedValue { public UUID generateId(String primaryLabel, Object entity) { return UUID.randomUUID(); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/src/main/java/org/springframework/data/neo4j/core/schema/Id.java index d232a251c..678993884 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/Id.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -25,9 +25,10 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; /** - * This annotation is included here for completeness. It marks an attribute as the primary id of a node entity. It can - * be used as an alternative to {@link org.springframework.data.annotation.Id} and it may provide additional features in - * the future. + * This annotation is included here for completeness. It marks an attribute as the primary + * id of a node entity. It can be used as an alternative to + * {@link org.springframework.data.annotation.Id} and it may provide additional features + * in the future. *

* To use assigned ids, annotate an arbitrary attribute of your domain class with * {@link org.springframework.data.annotation.Id} or this annotation: @@ -39,11 +40,12 @@ import org.apiguardian.api.API; * } * * - * You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node property in which the - * assigned id is stored. + * You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node + * property in which the assigned id is stored. *

- * To use internally generated ids, annotate an arbitrary attribute of type {@code java.lang.long} or - * {@code java.lang.Long} with {@code @Id} and {@link GeneratedValue @GeneratedValue}. + * To use internally generated ids, annotate an arbitrary attribute of type + * {@code java.lang.long} or {@code java.lang.Long} with {@code @Id} and + * {@link GeneratedValue @GeneratedValue}. * *

  * @Node
@@ -52,11 +54,13 @@ import org.apiguardian.api.API;
  * }
  * 
* - * It does not need to be named {@code id}, but most people chose this as the attribute in the class. As the attribute - * does not correspond to a node property, it cannot be renamed via {@code @Property}. + * It does not need to be named {@code id}, but most people chose this as the attribute in + * the class. As the attribute does not correspond to a node property, it cannot be + * renamed via {@code @Property}. *

- * To use externally generated ids, annotate an arbitrary attribute with a type that your generated returns with - * {@code @Id} and {@link GeneratedValue @GeneratedValue} and specify the generator class. + * To use externally generated ids, annotate an arbitrary attribute with a type that your + * generated returns with {@code @Id} and {@link GeneratedValue @GeneratedValue} and + * specify the generator class. * *

  * @Node
@@ -65,17 +69,18 @@ import org.apiguardian.api.API;
  * }
  * 
* - * Externally generated ids are indistinguishable to assigned ids from the database perspective and thus can be - * arbitrarily named via {@code @Property}. + * Externally generated ids are indistinguishable to assigned ids from the database + * perspective and thus can be arbitrarily named via {@code @Property}. * * @author Michael J. Simons * @since 6.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Documented @Inherited @org.springframework.data.annotation.Id @API(status = API.Status.STABLE, since = "6.0") public @interface Id { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java b/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java index 595e9cb74..3b0c86b18 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java @@ -20,8 +20,8 @@ import org.apiguardian.api.API; /** * Interface for generating ids for entities. * + * @param type of the id to generate * @author Michael J. Simons - * @param Type of the id to generate * @since 6.0 */ @FunctionalInterface @@ -30,9 +30,10 @@ public interface IdGenerator { /** * Generates a new id for given entity. - * + * @param primaryLabel the primary label under which the entity is registered * @param entity the entity to be saved * @return id to be assigned to the entity */ T generateId(String primaryLabel, Object entity); + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/Node.java b/src/main/java/org/springframework/data/neo4j/core/schema/Node.java index f3cd5c1d6..036605b74 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/Node.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/Node.java @@ -22,11 +22,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; + import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; /** - * The annotation to configure the mapping from a node with a given set of labels to a class and vice versa. + * The annotation to configure the mapping from a node with a given set of labels to a + * class and vice versa. * * @author Michael J. Simons * @since 6.0 @@ -39,21 +41,25 @@ import org.springframework.data.annotation.Persistent; public @interface Node { /** + * Returns all labels that constitutes this node. * @return See {@link #labels()}. */ @AliasFor("labels") String[] value() default {}; /** - * @return The labels to identify a node with that is supposed to be mapped to the class annotated with - * {@link Node @Node}. The first label will be the primary label if {@link #primaryLabel()} was not - * set explicitly. + * Returns all labels that constitutes this node. + * @return the labels to identify a node with that is supposed to be mapped to the + * class annotated with {@link Node @Node}. The first label will be the primary label + * if {@link #primaryLabel()} was not set explicitly. */ @AliasFor("value") String[] labels() default {}; /** + * Returns the primary label for this node. * @return The explicit primary label to identify a node. */ String primaryLabel() default ""; + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/PostLoad.java b/src/main/java/org/springframework/data/neo4j/core/schema/PostLoad.java index 82945443c..dc19fdd29 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/PostLoad.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/PostLoad.java @@ -24,8 +24,8 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; /** - * Informs SDN that the method annotated with this should be run once the object is loaded from the database and - * fully hydrated. + * Informs SDN that the method annotated with this should be run once the object is loaded + * from the database and fully hydrated. * * @author Michael J. Simons * @since 6.3.0 @@ -35,4 +35,5 @@ import org.apiguardian.api.API; @Inherited @API(status = API.Status.STABLE, since = "6.3.0") public @interface PostLoad { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/Property.java b/src/main/java/org/springframework/data/neo4j/core/schema/Property.java index f2c06afe6..ebe5e718d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/Property.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/Property.java @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; + import org.springframework.core.annotation.AliasFor; /** @@ -39,19 +40,24 @@ import org.springframework.core.annotation.AliasFor; public @interface Property { /** + * The name of this property in the graph. * @return See {@link #name()}. */ @AliasFor("name") String value() default ""; /** + * The name of this property in the graph. * @return The name of the property in the graph. */ @AliasFor("value") String name() default ""; /** - * @return Set this attribute to {@literal true} to prevent writing any value of this property to the graph. + * A flag if this property should be treated as read only. + * @return Set this attribute to {@literal true} to prevent writing any value of this + * property to the graph. */ boolean readOnly() default false; + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java b/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java index 47c3a466e..1cf90b2dd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; + import org.springframework.core.annotation.AliasFor; /** @@ -38,6 +39,42 @@ import org.springframework.core.annotation.AliasFor; @API(status = API.Status.STABLE, since = "6.0") public @interface Relationship { + /** + * Returns the type of the relationship. + * @return See {@link #type()}. + */ + @AliasFor("type") + String value() default ""; + + /** + * Returns the type of the relationship. + * @return The type of the relationship. + */ + @AliasFor("value") + String type() default ""; + + /** + * If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated + * with {@link Relationship} will be the target node of the relationship and the class + * containing the annotated attribute will be the start node. + *

+ * If {@code direction} is {@link Direction#INCOMING}, than the attribute annotated + * with {@link Relationship} will be the start node of the relationship and the class + * containing the annotated attribute will be the end node. + * @return The direction of the relationship. + */ + Direction direction() default Direction.OUTGOING; + + /** + * Set this attribute to {@literal false} if you don't want updates on an aggregate + * root to be cascaded to related objects. Be aware that in this case you are + * responsible to manually save the related objects and that you might end up with a + * local object graph that is not in sync with the actual graph. + * @return whether updates to the owning instance should be cascaded to the related + * objects + */ + boolean cascadeUpdates() default true; + /** * Enumeration of the direction a relationship can take. * @@ -56,39 +93,9 @@ public @interface Relationship { INCOMING; public Direction opposite() { - return this == OUTGOING ? INCOMING : OUTGOING; + return (this != OUTGOING) ? OUTGOING : INCOMING; } + } - /** - * @return See {@link #type()}. - */ - @AliasFor("type") - String value() default ""; - - /** - * @return The type of the relationship. - */ - @AliasFor("value") - String type() default ""; - - /** - * If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated with {@link Relationship} will be - * the target node of the relationship and the class containing the annotated attribute will be the start node. - *

- * If {@code direction} is {@link Direction#INCOMING}, than the attribute annotated with {@link Relationship} will be - * the start node of the relationship and the class containing the annotated attribute will be the end node. - * - * @return The direction of the relationship. - */ - Direction direction() default Direction.OUTGOING; - - /** - * Set this attribute to {@literal false} if you don't want updates on an aggregate root to be cascaded to related objects. - * Be aware that in this case you are responsible to manually save the related objects and that you might end up with a local - * object graph that is not in sync with the actual graph. - * - * @return whether updates to the owning instance should be cascaded to the related objects - */ - boolean cascadeUpdates() default true; } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipId.java b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipId.java index c6afcd42a..78e256fbd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipId.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipId.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.schema; -import org.apiguardian.api.API; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -24,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; + /** * A combined annotation for id fields in {@link RelationshipProperties} classes. * @@ -38,4 +38,5 @@ import java.lang.annotation.Target; @GeneratedValue @API(status = API.Status.STABLE, since = "6.2") public @interface RelationshipId { + } diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java index a2b9bd641..319ffbd8b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java @@ -25,9 +25,10 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; /** - * This marker interface is used on classes to mark that they represent additional relationship properties. A class that - * implements this interface must not be used as a or annotated with {@link Node}. It must however have exactly one field - * of type `Long` annotated with `@Id @GeneratedValue` such as this: + * This marker interface is used on classes to mark that they represent additional + * relationship properties. A class that implements this interface must not be used as a + * or annotated with {@link Node}. It must however have exactly one field of type `Long` + * annotated with `@Id @GeneratedValue` such as this: * *

  * @RelationshipProperties
@@ -52,12 +53,15 @@ import org.apiguardian.api.API;
 @Inherited
 @API(status = API.Status.STABLE, since = "6.0")
 public @interface RelationshipProperties {
+
 	/**
-	 * Set to true will persist {@link org.springframework.data.neo4j.core.mapping.Constants#NAME_OF_RELATIONSHIP_TYPE} to {@link Class#getSimpleName()}
-	 * as a property in relationships. This property will be used to determine the type of the relationship
-	 * when mapping back to the domain model.
-	 *
+	 * Set to true will persist
+	 * {@link org.springframework.data.neo4j.core.mapping.Constants#NAME_OF_RELATIONSHIP_TYPE}
+	 * to {@link Class#getSimpleName()} as a property in relationships. This property will
+	 * be used to determine the type of the relationship when mapping back to the domain
+	 * model.
 	 * @return whether to persist type information for the annotated class.
 	 */
 	boolean persistTypeInfo() default false;
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/TargetNode.java b/src/main/java/org/springframework/data/neo4j/core/schema/TargetNode.java
index a244200ea..bf0135786 100644
--- a/src/main/java/org/springframework/data/neo4j/core/schema/TargetNode.java
+++ b/src/main/java/org/springframework/data/neo4j/core/schema/TargetNode.java
@@ -24,10 +24,10 @@ import java.lang.annotation.Target;
  * Marks an entity in a {@link RelationshipProperties} as the target node.
  *
  * @author Gerrit Meier
- * @soundtrack Goldfinger - Here in your bedroom
  * @since 6.0
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.FIELD)
 public @interface TargetNode {
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java b/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java
index 49d5002cc..b55dab57f 100644
--- a/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java
+++ b/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java
@@ -1,6 +1,22 @@
+/*
+ * Copyright 2011-2025 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.
+ */
 /**
- * This package contains the schema that is defined by a set of classes, representing nodes and relationships and their
- * properties. It provides Neo4js main annotations to mark classes as persistable nodes.
+ * This package contains the schema that is defined by a set of classes, representing
+ * nodes and relationships and their properties. It provides Neo4js main annotations to
+ * mark classes as persistable nodes.
  *
  * @author Michael J. Simons
  */
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java b/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java
index bab837461..13da8c89a 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/BookmarkManagerReference.java
@@ -19,6 +19,7 @@ import java.util.Objects;
 import java.util.function.Supplier;
 
 import org.jspecify.annotations.Nullable;
+
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.ObjectProvider;
@@ -43,14 +44,12 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
 		}
 
 		@Override
-		@Nullable
-		public Neo4jBookmarkManager getIfAvailable() throws BeansException {
+		@Nullable public Neo4jBookmarkManager getIfAvailable() throws BeansException {
 			return null;
 		}
 
 		@Override
-		@Nullable
-		public Neo4jBookmarkManager getIfUnique() throws BeansException {
+		@Nullable public Neo4jBookmarkManager getIfUnique() throws BeansException {
 			return null;
 		}
 
@@ -66,7 +65,8 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
 	@Nullable
 	private ApplicationEventPublisher applicationEventPublisher;
 
-	public BookmarkManagerReference(Supplier defaultBookmarkManagerSupplier, @Nullable Neo4jBookmarkManager bookmarkManager) {
+	public BookmarkManagerReference(Supplier defaultBookmarkManagerSupplier,
+			@Nullable Neo4jBookmarkManager bookmarkManager) {
 		this.defaultBookmarkManagerSupplier = defaultBookmarkManagerSupplier;
 		this.bookmarkManager = bookmarkManager;
 	}
@@ -77,8 +77,7 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
 		this.neo4jBookmarkManagers = applicationContext.getBeanProvider(Neo4jBookmarkManager.class);
 		this.applicationEventPublisher = applicationContext;
 		if (this.bookmarkManager != null) {
-			Objects.requireNonNull(this.bookmarkManager)
-					.setApplicationEventPublisher(this.applicationEventPublisher);
+			Objects.requireNonNull(this.bookmarkManager).setApplicationEventPublisher(this.applicationEventPublisher);
 		}
 	}
 
@@ -88,8 +87,9 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
 			synchronized (this) {
 				result = this.bookmarkManager;
 				if (result == null) {
-					this.bookmarkManager = neo4jBookmarkManagers.getIfAvailable(this.defaultBookmarkManagerSupplier);
-					//noinspection DataFlowIssue
+					this.bookmarkManager = this.neo4jBookmarkManagers
+						.getIfAvailable(this.defaultBookmarkManagerSupplier);
+					// noinspection DataFlowIssue
 					this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher);
 					result = this.bookmarkManager;
 				}
@@ -97,4 +97,5 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
 		}
 		return result;
 	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java b/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java
index 26ed2b660..57a10031e 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/DateLong.java
@@ -20,22 +20,16 @@ import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-import java.util.Date;
 
 import org.apiguardian.api.API;
-import org.jspecify.annotations.Nullable;
-import org.neo4j.driver.Value;
-import org.neo4j.driver.Values;
-import org.neo4j.driver.types.TypeSystem;
+
 import org.springframework.data.neo4j.core.convert.ConvertWith;
-import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
 
 /**
- * Indicates SDN to store dates as long in the database.
- * Applicable to `java.util.Date` and `java.time.Instant`
+ * Indicates SDN to store dates as long in the database. Applicable to `java.util.Date`
+ * and `java.time.Instant`
  *
  * @author Michael J. Simons
- * @soundtrack Linkin Park - One More Light Live
  * @since 6.0
  */
 @Retention(RetentionPolicy.RUNTIME)
@@ -44,18 +38,5 @@ import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConver
 @ConvertWith(converter = DateLongConverter.class)
 @API(status = API.Status.STABLE, since = "6.0")
 public @interface DateLong {
-}
-
-final class DateLongConverter implements Neo4jPersistentPropertyConverter {
-
-	@Override
-	public Value write(@Nullable Date source) {
-		return source == null ? Values.NULL : Values.value(source.getTime());
-	}
-
-	@Override
-	@Nullable
-	public Date read(@Nullable Value source) {
-		return source == null || TypeSystem.getDefault().NULL().isTypeOf(source) ? null : new Date(source.asLong());
-	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateLongConverter.java b/src/main/java/org/springframework/data/neo4j/core/support/DateLongConverter.java
new file mode 100644
index 000000000..b5c7d8078
--- /dev/null
+++ b/src/main/java/org/springframework/data/neo4j/core/support/DateLongConverter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2011-2025 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.core.support;
+
+import java.util.Date;
+
+import org.jspecify.annotations.Nullable;
+import org.neo4j.driver.Value;
+import org.neo4j.driver.Values;
+import org.neo4j.driver.types.TypeSystem;
+
+import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
+
+final class DateLongConverter implements Neo4jPersistentPropertyConverter {
+
+	@Override
+	public Value write(@Nullable Date source) {
+		return (source != null) ? Values.value(source.getTime()) : Values.NULL;
+	}
+
+	@Override
+	@Nullable public Date read(@Nullable Value source) {
+		return (source != null && !TypeSystem.getDefault().NULL().isTypeOf(source)) ? new Date(source.asLong()) : null;
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateString.java b/src/main/java/org/springframework/data/neo4j/core/support/DateString.java
index a578a946f..f68d9feb4 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/DateString.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/DateString.java
@@ -20,28 +20,18 @@ import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import java.util.Date;
-import java.util.TimeZone;
 
 import org.apiguardian.api.API;
-import org.jspecify.annotations.Nullable;
-import org.neo4j.driver.Value;
-import org.neo4j.driver.Values;
-import org.neo4j.driver.types.TypeSystem;
+
 import org.springframework.core.annotation.AliasFor;
 import org.springframework.data.neo4j.core.convert.ConvertWith;
-import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
-import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
-import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
 
 /**
- * Indicates SDN 6 to store dates as {@link String} in the database. Applicable to {@link Date} and
- * {@link java.time.Instant}.
+ * Indicates SDN 6 to store dates as {@link String} in the database. Applicable to
+ * {@link Date} and {@link java.time.Instant}.
  *
  * @author Michael J. Simons
- * @soundtrack Metallica - S&M2
  * @since 6.0
  */
 @Retention(RetentionPolicy.RUNTIME)
@@ -51,8 +41,14 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
 @API(status = API.Status.STABLE, since = "6.0")
 public @interface DateString {
 
+	/**
+	 * Pattern conforming to an ISO 8601 date time string (without timezone).
+	 */
 	String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
 
+	/**
+	 * The ID of the default timezone to use.
+	 */
 	String DEFAULT_ZONE_ID = "UTC";
 
 	@AliasFor("format")
@@ -62,58 +58,13 @@ public @interface DateString {
 	String format() default ISO_8601;
 
 	/**
-	 * Some temporals like {@link java.time.Instant}, representing an instantaneous point in time cannot be formatted
-	 * with a given {@link java.time.ZoneId}. In case you want to format an instant or similar with a default pattern,
-	 * we assume a zone with the given id and default to {@literal UTC} which is the same assumption that the predefined
-	 * patterns in {@link java.time.format.DateTimeFormatter} take.
-	 *
+	 * Some temporals like {@link java.time.Instant}, representing an instantaneous point
+	 * in time cannot be formatted with a given {@link java.time.ZoneId}. In case you want
+	 * to format an instant or similar with a default pattern, we assume a zone with the
+	 * given id and default to {@literal UTC} which is the same assumption that the
+	 * predefined patterns in {@link java.time.format.DateTimeFormatter} take.
 	 * @return The zone id to use when applying a custom pattern to an instant temporal.
 	 */
 	String zoneId() default DEFAULT_ZONE_ID;
-}
-
-final class DateStringConverterFactory implements Neo4jPersistentPropertyConverterFactory {
-
-	@Override
-	public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) {
-
-		if (persistentProperty.getActualType() == Date.class) {
-			DateString config = persistentProperty.getRequiredAnnotation(DateString.class);
-			return new DateStringConverter(config.value());
-		} else {
-			throw new UnsupportedOperationException(
-					"Other types than java.util.Date are not yet supported; please file a ticket");
-		}
-	}
-}
-
-final class DateStringConverter implements Neo4jPersistentPropertyConverter {
-
-	private final String format;
-
-	DateStringConverter(String format) {
-		this.format = format;
-	}
-
-	private SimpleDateFormat getFormat() {
-		SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
-		simpleDateFormat.setTimeZone(TimeZone.getTimeZone(DateString.DEFAULT_ZONE_ID));
-		return simpleDateFormat;
-	}
-
-	@Override
-	public Value write(@Nullable Date source) {
-		return source == null ? Values.NULL : Values.value(getFormat().format(source));
-	}
-
-	@Override
-	@Nullable
-	public Date read(@Nullable Value source) {
-		try {
-			return source == null || TypeSystem.getDefault().NULL().isTypeOf(source) ? null : getFormat().parse(source.asString());
-		} catch (ParseException e) {
-			throw new RuntimeException(e);
-		}
-	}
 
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverter.java b/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverter.java
new file mode 100644
index 000000000..46885eeb5
--- /dev/null
+++ b/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011-2025 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.core.support;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.jspecify.annotations.Nullable;
+import org.neo4j.driver.Value;
+import org.neo4j.driver.Values;
+import org.neo4j.driver.types.TypeSystem;
+
+import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
+
+final class DateStringConverter implements Neo4jPersistentPropertyConverter {
+
+	private final String format;
+
+	DateStringConverter(String format) {
+		this.format = format;
+	}
+
+	private SimpleDateFormat getFormat() {
+		SimpleDateFormat simpleDateFormat = new SimpleDateFormat(this.format);
+		simpleDateFormat.setTimeZone(TimeZone.getTimeZone(DateString.DEFAULT_ZONE_ID));
+		return simpleDateFormat;
+	}
+
+	@Override
+	public Value write(@Nullable Date source) {
+		return (source != null) ? Values.value(getFormat().format(source)) : Values.NULL;
+	}
+
+	@Override
+	@Nullable public Date read(@Nullable Value source) {
+		try {
+			return (source == null || TypeSystem.getDefault().NULL().isTypeOf(source)) ? null
+					: getFormat().parse(source.asString());
+		}
+		catch (ParseException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverterFactory.java b/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverterFactory.java
new file mode 100644
index 000000000..8429182b0
--- /dev/null
+++ b/src/main/java/org/springframework/data/neo4j/core/support/DateStringConverterFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2011-2025 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.core.support;
+
+import java.util.Date;
+
+import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
+import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
+import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
+
+final class DateStringConverterFactory implements Neo4jPersistentPropertyConverterFactory {
+
+	@Override
+	public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) {
+
+		if (persistentProperty.getActualType() == Date.class) {
+			DateString config = persistentProperty.getRequiredAnnotation(DateString.class);
+			return new DateStringConverter(config.value());
+		}
+		else {
+			throw new UnsupportedOperationException(
+					"Other types than java.util.Date are not yet supported; please file a ticket");
+		}
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicate.java b/src/main/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicate.java
index 64e138815..a82493f65 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicate.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicate.java
@@ -24,27 +24,33 @@ import org.neo4j.driver.exceptions.RetryableException;
 import org.neo4j.driver.exceptions.ServiceUnavailableException;
 import org.neo4j.driver.exceptions.SessionExpiredException;
 import org.neo4j.driver.exceptions.TransientException;
+
 import org.springframework.dao.TransientDataAccessResourceException;
 import org.springframework.transaction.TransactionSystemException;
 
-
 /**
- * A predicate indicating {@literal true} for {@link Throwable throwables} that can be safely retried and {@literal false}
- * in any other case. This predicate can be used for example with Resilience4j.
+ * A predicate indicating {@literal true} for {@link Throwable throwables} that can be
+ * safely retried and {@literal false} in any other case. This predicate can be used for
+ * example with Resilience4j.
  *
  * @author Michael J. Simons
- * @soundtrack The Kleptones - 24 Hours
  * @since 6.0
  */
 @API(status = API.Status.STABLE, since = "6.0")
 public final class RetryExceptionPredicate implements Predicate {
 
+	/**
+	 * A known message indicating retryable errors before they had been marked retryable.
+	 */
 	public static final String TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED = "Transaction must be open, but has already been closed";
+
+	/**
+	 * A known message indicating retryable errors before they had been marked retryable.
+	 */
 	public static final String SESSION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED = "Session must be open, but has already been closed";
-	private static final Set RETRYABLE_ILLEGAL_STATE_MESSAGES = Set.of(
-			TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED,
-			SESSION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED
-	);
+
+	private static final Set RETRYABLE_ILLEGAL_STATE_MESSAGES = Set
+		.of(TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED, SESSION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED);
 
 	@Override
 	public boolean test(Throwable throwable) {
@@ -63,16 +69,20 @@ public final class RetryExceptionPredicate implements Predicate {
 		}
 
 		Throwable ex = throwable;
-		if (throwable instanceof TransientDataAccessResourceException || throwable instanceof TransactionSystemException) {
+		if (throwable instanceof TransientDataAccessResourceException
+				|| throwable instanceof TransactionSystemException) {
 			ex = throwable.getCause();
 		}
 
 		if (ex instanceof TransientException) {
 			String code = ((TransientException) ex).code();
-			return !("Neo.TransientError.Transaction.Terminated".equals(code) ||
-					"Neo.TransientError.Transaction.LockClientStopped".equals(code));
-		} else {
-			return ex instanceof SessionExpiredException || ex instanceof ServiceUnavailableException || ex instanceof DiscoveryException;
+			return !("Neo.TransientError.Transaction.Terminated".equals(code)
+					|| "Neo.TransientError.Transaction.LockClientStopped".equals(code));
+		}
+		else {
+			return ex instanceof SessionExpiredException || ex instanceof ServiceUnavailableException
+					|| ex instanceof DiscoveryException;
 		}
 	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/UUIDStringGenerator.java b/src/main/java/org/springframework/data/neo4j/core/support/UUIDStringGenerator.java
index 5903517b1..89e0962dc 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/UUIDStringGenerator.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/UUIDStringGenerator.java
@@ -18,13 +18,13 @@ package org.springframework.data.neo4j.core.support;
 import java.util.UUID;
 
 import org.apiguardian.api.API;
+
 import org.springframework.data.neo4j.core.schema.IdGenerator;
 
 /**
  * A generator providing UUIDs.
  *
  * @author Michael J. Simons
- * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack)
  * @since 6.0
  */
 @API(status = API.Status.STABLE, since = "6.0")
@@ -34,4 +34,5 @@ public final class UUIDStringGenerator implements IdGenerator {
 	public String generateId(String primaryLabel, Object entity) {
 		return UUID.randomUUID().toString();
 	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/UserAgent.java b/src/main/java/org/springframework/data/neo4j/core/support/UserAgent.java
index 71996db86..4dbb07ad0 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/UserAgent.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/UserAgent.java
@@ -17,22 +17,24 @@ package org.springframework.data.neo4j.core.support;
 
 import org.jspecify.annotations.Nullable;
 import org.neo4j.driver.Driver;
+
 import org.springframework.data.mapping.context.AbstractMappingContext;
 import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
 
 /**
- * Representation of a user agent containing sensible information to identify queries generated by or executed via Spring Data Neo4j.
+ * Representation of a user agent containing sensible information to identify queries
+ * generated by or executed via Spring Data Neo4j.
  *
  * @author Michael J. Simons
  * @since 6.1.11
  */
 public enum UserAgent {
 
-	INSTANCE(
-			getVersionOf(Driver.class),
-			getVersionOf(AbstractMappingContext.class),
-			getVersionOf(EnableNeo4jRepositories.class)
-	);
+	/**
+	 * The single instance to describe the SDN user agent.
+	 */
+	INSTANCE(getVersionOf(Driver.class), getVersionOf(AbstractMappingContext.class),
+			getVersionOf(EnableNeo4jRepositories.class));
 
 	@Nullable
 	private final String driverVersion;
@@ -46,42 +48,22 @@ public enum UserAgent {
 	private final String representation;
 
 	UserAgent(@Nullable String driverVersion, @Nullable String springDataVersion, @Nullable String sdnVersion) {
-		int idxOfDash = driverVersion == null ? -1 : driverVersion.indexOf('-');
-		this.driverVersion = driverVersion == null ?
-				null :
-				driverVersion.substring(0, idxOfDash > 0 ? idxOfDash : driverVersion.length());
+		int idxOfDash = (driverVersion != null) ? driverVersion.indexOf('-') : -1;
+		this.driverVersion = (driverVersion != null)
+				? driverVersion.substring(0, (idxOfDash > 0) ? idxOfDash : driverVersion.length()) : null;
 		this.springDataVersion = springDataVersion;
 		this.sdnVersion = sdnVersion;
 
 		String unknown = "-";
 		this.representation = String.format("Java/%s (%s %s %s) neo4j-java/%s spring-data/%s spring-data-neo4j/%s",
-				System.getProperty("java.version"),
-				System.getProperty("java.vm.vendor"),
-				System.getProperty("java.vm.name"),
-				System.getProperty("java.vm.version"),
-				this.driverVersion == null ? unknown : this.driverVersion,
-				this.springDataVersion == null ? unknown : this.springDataVersion,
-				this.sdnVersion == null ? unknown : this.sdnVersion
-		);
+				System.getProperty("java.version"), System.getProperty("java.vm.vendor"),
+				System.getProperty("java.vm.name"), System.getProperty("java.vm.version"),
+				(this.driverVersion != null) ? this.driverVersion : unknown,
+				(this.springDataVersion != null) ? this.springDataVersion : unknown,
+				(this.sdnVersion != null) ? this.sdnVersion : unknown);
 	}
 
-	@Nullable
-	public String getDriverVersion() {
-		return driverVersion;
-	}
-
-	@Nullable
-	public String getSpringDataVersion() {
-		return springDataVersion;
-	}
-
-	@Nullable
-	public String getSdnVersion() {
-		return sdnVersion;
-	}
-
-	@Nullable
-	private static String getVersionOf(Class type) {
+	@Nullable private static String getVersionOf(Class type) {
 
 		Package p = type.getPackage();
 		String version = p.getImplementationVersion();
@@ -91,8 +73,21 @@ public enum UserAgent {
 		return null;
 	}
 
+	@Nullable public String getDriverVersion() {
+		return this.driverVersion;
+	}
+
+	@Nullable public String getSpringDataVersion() {
+		return this.springDataVersion;
+	}
+
+	@Nullable public String getSdnVersion() {
+		return this.sdnVersion;
+	}
+
 	@Override
 	public String toString() {
 		return this.representation;
 	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/support/package-info.java b/src/main/java/org/springframework/data/neo4j/core/support/package-info.java
index cae818161..8a89709dc 100644
--- a/src/main/java/org/springframework/data/neo4j/core/support/package-info.java
+++ b/src/main/java/org/springframework/data/neo4j/core/support/package-info.java
@@ -1,8 +1,22 @@
+/*
+ * Copyright 2011-2025 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.
+ */
 /**
- * 
-   This package provides a couple of support classes that might be helpful in your domain, for example a predicate
-   indicating that some transaction may be retried and additional converters and id generators.
- * 
+ *  This package provides a couple of support classes that might be
+ * helpful in your domain, for example a predicate indicating that some transaction may be
+ * retried and additional converters and id generators. 
  */
 @NullMarked
 package org.springframework.data.neo4j.core.support;
diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/AbstractBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/AbstractBookmarkManager.java
index a7d2bb5f1..fe1e62338 100644
--- a/src/main/java/org/springframework/data/neo4j/core/transaction/AbstractBookmarkManager.java
+++ b/src/main/java/org/springframework/data/neo4j/core/transaction/AbstractBookmarkManager.java
@@ -20,5 +20,6 @@ package org.springframework.data.neo4j.core.transaction;
  *
  * @author Michael J. Simons
  */
-non-sealed abstract class AbstractBookmarkManager implements Neo4jBookmarkManager {
+abstract non-sealed class AbstractBookmarkManager implements Neo4jBookmarkManager {
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java
index a740152e7..f92e8efde 100644
--- a/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java
+++ b/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultBookmarkManager.java
@@ -26,13 +26,13 @@ import java.util.function.Supplier;
 
 import org.jspecify.annotations.Nullable;
 import org.neo4j.driver.Bookmark;
+
 import org.springframework.context.ApplicationEventPublisher;
 
 /**
  * Default bookmark manager.
  *
  * @author Michael J. Simons
- * @soundtrack Helge Schneider - The Last Jazz
  * @since 7.0
  */
 final class DefaultBookmarkManager extends AbstractBookmarkManager {
@@ -40,8 +40,10 @@ final class DefaultBookmarkManager extends AbstractBookmarkManager {
 	private final Set bookmarks = new HashSet<>();
 
 	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-	private final Lock read = lock.readLock();
-	private final Lock write = lock.writeLock();
+
+	private final Lock read = this.lock.readLock();
+
+	private final Lock write = this.lock.writeLock();
 
 	private final Supplier> bookmarksSupplier;
 
@@ -49,19 +51,20 @@ final class DefaultBookmarkManager extends AbstractBookmarkManager {
 	private ApplicationEventPublisher applicationEventPublisher;
 
 	DefaultBookmarkManager(@Nullable Supplier> bookmarksSupplier) {
-		this.bookmarksSupplier = bookmarksSupplier == null ? Collections::emptySet : bookmarksSupplier;
+		this.bookmarksSupplier = (bookmarksSupplier != null) ? bookmarksSupplier : Collections::emptySet;
 	}
 
 	@Override
 	public Collection getBookmarks() {
 
 		try {
-			read.lock();
+			this.read.lock();
 			HashSet bookmarksToUse = new HashSet<>(this.bookmarks);
-			bookmarksToUse.addAll(bookmarksSupplier.get());
+			bookmarksToUse.addAll(this.bookmarksSupplier.get());
 			return Collections.unmodifiableSet(bookmarksToUse);
-		} finally {
-			read.unlock();
+		}
+		finally {
+			this.read.unlock();
 		}
 	}
 
@@ -69,14 +72,16 @@ final class DefaultBookmarkManager extends AbstractBookmarkManager {
 	public void updateBookmarks(Collection usedBookmarks, Collection newBookmarks) {
 
 		try {
-			write.lock();
-			bookmarks.removeAll(usedBookmarks);
-			newBookmarks.stream().filter(Objects::nonNull).forEach(bookmarks::add);
-			if (applicationEventPublisher != null) {
-				applicationEventPublisher.publishEvent(new Neo4jBookmarksUpdatedEvent(new HashSet<>(bookmarks)));
+			this.write.lock();
+			this.bookmarks.removeAll(usedBookmarks);
+			newBookmarks.stream().filter(Objects::nonNull).forEach(this.bookmarks::add);
+			if (this.applicationEventPublisher != null) {
+				this.applicationEventPublisher
+					.publishEvent(new Neo4jBookmarksUpdatedEvent(new HashSet<>(this.bookmarks)));
 			}
-		} finally {
-			write.unlock();
+		}
+		finally {
+			this.write.unlock();
 		}
 	}
 
@@ -84,4 +89,5 @@ final class DefaultBookmarkManager extends AbstractBookmarkManager {
 	public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) {
 		this.applicationEventPublisher = applicationEventPublisher;
 	}
+
 }
diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java
index 92203cd3e..4c404a7f6 100644
--- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java
+++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManager.java
@@ -22,26 +22,28 @@ import java.util.function.Supplier;
 import org.apiguardian.api.API;
 import org.jspecify.annotations.Nullable;
 import org.neo4j.driver.Bookmark;
+
 import org.springframework.context.ApplicationEventPublisher;
 
 /**
  * Responsible for storing, updating and retrieving the bookmarks of Neo4j's transaction.
  *
  * @author Michael J. Simons
- * @soundtrack Metallica - Death Magnetic
  * @since 6.0
  */
 @API(status = API.Status.STABLE, since = "6.1.1")
 public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, NoopBookmarkManager {
 
 	/**
-	 * {@return the default bookmark manager}
+	 * Returns the default bookmark manager.
+	 * @return the default bookmark manager
 	 */
 	static Neo4jBookmarkManager create() {
 		return new DefaultBookmarkManager(null);
 	}
 
 	/**
+	 * Returns the default reactive version of bookmark manager.
 	 * @return default reactive version of bookmark manager
 	 */
 	static Neo4jBookmarkManager createReactive() {
@@ -49,42 +51,45 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No
 	}
 
 	/**
-	 * Use this factory method to add supplier of initial "seeding" bookmarks to the transaction managers
+	 * Use this factory method to add supplier of initial "seeding" bookmarks to the
+	 * transaction managers
 	 * 

- * While this class will make sure that the supplier will be accessed in a thread-safe manner, - * it is the caller's duty to provide a thread safe supplier (not changing the seed during a call, etc.). - * - * @param bookmarksSupplier A supplier for seeding bookmarks, can be null. The supplier is free to provide different - * bookmarks on each call. - * @return A bookmark manager + * While this class will make sure that the supplier will be accessed in a thread-safe + * manner, it is the caller's duty to provide a thread safe supplier (not changing the + * seed during a call, etc.). + * @param bookmarksSupplier a supplier for seeding bookmarks, can be null. The + * supplier is free to provide different bookmarks on each call. + * @return a bookmark manager */ static Neo4jBookmarkManager create(Supplier> bookmarksSupplier) { return new DefaultBookmarkManager(bookmarksSupplier); } /** - * Use this factory method to add supplier of initial "seeding" bookmarks to the transaction managers + * Use this factory method to add supplier of initial "seeding" bookmarks to the + * transaction managers *

- * While this class will make sure that the supplier will be accessed in a thread-safe manner, - * it is the caller's duty to provide a thread safe supplier (not changing the seed during a call, etc.). - * - * @param bookmarksSupplier A supplier for seeding bookmarks, can be null. The supplier is free to provide different - * bookmarks on each call. - * @return A reactive bookmark manager + * While this class will make sure that the supplier will be accessed in a thread-safe + * manner, it is the caller's duty to provide a thread safe supplier (not changing the + * seed during a call, etc.). + * @param bookmarksSupplier a supplier for seeding bookmarks, can be null. The + * supplier is free to provide different bookmarks on each call + * @return a reactive bookmark manager */ static Neo4jBookmarkManager createReactive(Supplier> bookmarksSupplier) { return new ReactiveDefaultBookmarkManager(bookmarksSupplier); } /** - * Use this bookmark manager at your own risk, it will effectively disable any bookmark management by dropping all - * bookmarks and never supplying any. In a cluster you will be at a high risk of experiencing stale reads. In a single - * instance it will most likely not make any difference. + * Use this bookmark manager at your own risk, it will effectively disable any + * bookmark management by dropping all bookmarks and never supplying any. In a cluster + * you will be at a high risk of experiencing stale reads. In a single instance it + * will most likely not make any difference. *

- * In a cluster this can be a sensible approach only and if only you can tolerate stale reads and are not in danger of - * overwriting old data. - * - * @return A noop bookmark manager, dropping new bookmarks immediately, never supplying bookmarks. + * In a cluster this can be a sensible approach only and if only you can tolerate + * stale reads and are not in danger of overwriting old data. + * @return a noop bookmark manager, dropping new bookmarks immediately, never + * supplying bookmarks. * @since 6.1.11 */ @API(status = API.Status.STABLE, since = "6.1.11") @@ -93,28 +98,29 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No } /** - * No need to introspect this collection ever. The Neo4j driver will together with the cluster figure out which of - * the bookmarks is the most recent one. - * + * No need to introspect this collection ever. The Neo4j driver will together with the + * cluster figure out which of the bookmarks is the most recent one. * @return a collection of currently known bookmarks */ Collection getBookmarks(); /** - * Refreshes the bookmark manager with the {@code newBookmarks new bookmarks} received after the last transaction - * committed. The collection of {@code usedBookmarks} should be removed from the list of known bookmarks. - * - * @param usedBookmarks The collection of bookmarks known prior to the end of a transaction - * @param newBookmarks The bookmarks received after the end of a transaction + * Refreshes the bookmark manager with the {@code newBookmarks new bookmarks} received + * after the last transaction committed. The collection of {@code usedBookmarks} + * should be removed from the list of known bookmarks. + * @param usedBookmarks the collection of bookmarks known prior to the end of a + * transaction + * @param newBookmarks the bookmarks received after the end of a transaction * @see #updateBookmarks(Collection, Collection) */ void updateBookmarks(Collection usedBookmarks, Collection newBookmarks); /** * A hook for bookmark managers supporting events. - * - * @param applicationEventPublisher An event publisher. If null, no events will be published. + * @param applicationEventPublisher an event publisher. If null, no events will be + * published. */ default void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) { } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java index 43264836c..2ceb81632 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarksUpdatedEvent.java @@ -22,14 +22,14 @@ import java.util.Set; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; + import org.springframework.context.ApplicationEvent; /** - * This event will be published after a Neo4j transaction manager physically committed a transaction without errors - * and received a new set of bookmarks from the cluster. + * This event will be published after a Neo4j transaction manager physically committed a + * transaction without errors and received a new set of bookmarks from the cluster. * * @author Michael J. Simons - * @soundtrack Black Sabbath - Master Of Reality * @since 6.1.1 */ @API(status = API.Status.STABLE, since = "6.1.1") @@ -37,8 +37,8 @@ public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent { @Serial private static final long serialVersionUID = 2143476552056698819L; - @Nullable - private transient final Set bookmarks; + + private final transient @Nullable Set bookmarks; Neo4jBookmarksUpdatedEvent(Set bookmarks) { super(bookmarks); @@ -46,10 +46,12 @@ public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent { } /** - * @return An unmodifiable views of the new bookmarks. + * Retrieves the set of bookmarks associated with this event. + * @return an unmodifiable views of the new bookmarks */ public Set getBookmarks() { - return this.bookmarks == null ? Set.of() : Collections.unmodifiableSet(this.bookmarks); + return (this.bookmarks != null) ? Collections.unmodifiableSet(this.bookmarks) : Set.of(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java index c105a30aa..93c5c592b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -16,12 +16,14 @@ package org.springframework.data.neo4j.core.transaction; import org.neo4j.driver.Driver; + import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronization; /** - * Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when - * participating in a non-native Neo4j transaction, such as a Jta transaction. + * Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of + * a transaction when participating in a non-native Neo4j transaction, such as a Jta + * transaction. * * @author Gerrit Meier * @author Michael J. Simons @@ -37,19 +39,11 @@ final class Neo4jSessionSynchronization extends ResourceHolderSynchronization bookmarks) { + Neo4jTransactionContext(DatabaseSelection databaseSelection, UserSelection userSelection, + Collection bookmarks) { this.databaseSelection = databaseSelection; this.userSelection = userSelection; this.bookmarks = bookmarks; } DatabaseSelection getDatabaseSelection() { - return databaseSelection; + return this.databaseSelection; } UserSelection getUserSelection() { - return userSelection; + return this.userSelection; } Collection getBookmarks() { - return bookmarks; + return this.bookmarks; } - /** - * @param inDatabase Target database - * @param asUser A Neo4j user - * @return True if the combination of target database and impersonated user is the same in this context as for the given arguments. - */ boolean isForDatabaseAndUser(DatabaseSelection inDatabase, UserSelection asUser) { return this.databaseSelection.equals(inDatabase) && this.userSelection.equals(asUser); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java index c79e16a1f..2476e8c45 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java @@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.support.RetryExceptionPredicate; @@ -28,8 +29,9 @@ import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; /** - * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.Transaction}. - * {@link Neo4jTransactionManager} binds instances of this class to the thread. + * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a + * {@link org.neo4j.driver.Transaction}. {@link Neo4jTransactionManager} binds instances + * of this class to the thread. *

* Note: Intended for internal usage only. * @@ -39,12 +41,15 @@ import org.springframework.util.Assert; final class Neo4jTransactionHolder extends ResourceHolderSupport { private final Neo4jTransactionContext context; + /** * The ongoing session... */ private final Session session; + /** - * The driver's transaction as the second building block of what to synchronize our transaction against. + * The driver's transaction as the second building block of what to synchronize our + * transaction against. */ private final Transaction transaction; @@ -56,34 +61,35 @@ final class Neo4jTransactionHolder extends ResourceHolderSupport { } /** - * Returns the transaction if it has been opened in a session for the requested database or an empty optional. - * + * Returns the transaction if it has been opened in a session for the requested + * database or an empty optional. * @param inDatabase selected database to use * @param asUser impersonated user if any - * @return An optional, ongoing transaction. + * @return an optional, ongoing transaction. */ - @Nullable - Transaction getTransaction(DatabaseSelection inDatabase, UserSelection asUser) { - return this.context.isForDatabaseAndUser(inDatabase, asUser) ? transaction : null; + @Nullable Transaction getTransaction(DatabaseSelection inDatabase, UserSelection asUser) { + return this.context.isForDatabaseAndUser(inDatabase, asUser) ? this.transaction : null; } Collection commit() { - Assert.state(hasActiveTransaction(), RetryExceptionPredicate.TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED); + Assert.state(hasActiveTransaction(), + RetryExceptionPredicate.TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED); Assert.state(!isRollbackOnly(), "Resource must not be marked as rollback only"); - transaction.commit(); - transaction.close(); + this.transaction.commit(); + this.transaction.close(); - return session.lastBookmarks(); + return this.session.lastBookmarks(); } void rollback() { - Assert.state(hasActiveTransaction(), RetryExceptionPredicate.TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED); + Assert.state(hasActiveTransaction(), + RetryExceptionPredicate.TRANSACTION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED); - transaction.rollback(); - transaction.close(); + this.transaction.rollback(); + this.transaction.close(); } void close() { @@ -91,30 +97,31 @@ final class Neo4jTransactionHolder extends ResourceHolderSupport { Assert.state(hasActiveSession(), RetryExceptionPredicate.SESSION_MUST_BE_OPEN_BUT_HAS_ALREADY_BEEN_CLOSED); if (hasActiveTransaction()) { - transaction.close(); + this.transaction.close(); } - session.close(); + this.session.close(); } boolean hasActiveSession() { - return session.isOpen(); + return this.session.isOpen(); } boolean hasActiveTransaction() { - return transaction.isOpen(); + return this.transaction.isOpen(); } DatabaseSelection getDatabaseSelection() { - return context.getDatabaseSelection(); + return this.context.getDatabaseSelection(); } UserSelection getUserSelection() { - return context.getUserSelection(); + return this.context.getUserSelection(); } Collection getBookmarks() { - return context.getBookmarks(); + return this.context.getBookmarks(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 28367c4d4..093b3af4b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -28,6 +28,7 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.RetryableException; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -47,22 +48,88 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.util.Assert; /** - * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This - * transaction manager will synchronize a pair of a native Neo4j session/transaction with the transaction. + * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native + * Neo4j transactions. This transaction manager will synchronize a pair of a native Neo4j + * session/transaction with the transaction. * * @author Michael J. Simons * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") -public final class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements ApplicationContextAware { +public final class Neo4jTransactionManager extends AbstractPlatformTransactionManager + implements ApplicationContextAware { @Serial private static final long serialVersionUID = 7971369288503005574L; + /** + * The underlying driver, which is also the synchronisation object. + */ + private final transient Driver driver; + + /** + * Database name provider. + */ + private final transient DatabaseSelectionProvider databaseSelectionProvider; + + /** + * Provider for user impersonation. + */ + private final transient UserSelectionProvider userSelectionProvider; + + private final transient BookmarkManagerReference bookmarkManager; + + /** + * This will create a transaction manager for the default database. + * @param driver a driver instance + */ + public Neo4jTransactionManager(Driver driver) { + + this(with(driver)); + } + + /** + * This will create a transaction manager targeting whatever the database selection + * provider determines. + * @param driver a driver instance + * @param databaseSelectionProvider the database selection provider to determine the + * database in which the transactions should happen + */ + public Neo4jTransactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { + + this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider)); + } + + /** + * This constructor can be used to configure the bookmark manager being used. It is + * useful when you need to seed the bookmark manager or if you want to capture new + * bookmarks. + * @param driver a driver instance + * @param databaseSelectionProvider the database selection provider to determine the + * database in which the transactions should happen + * @param bookmarkManager a bookmark manager + */ + public Neo4jTransactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider, + Neo4jBookmarkManager bookmarkManager) { + + this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider) + .withBookmarkManager(bookmarkManager)); + } + + private Neo4jTransactionManager(Builder builder) { + + this.driver = builder.driver; + this.databaseSelectionProvider = (builder.databaseSelectionProvider != null) ? builder.databaseSelectionProvider + : DatabaseSelectionProvider.getDefaultSelectionProvider(); + this.userSelectionProvider = (builder.userSelectionProvider != null) ? builder.userSelectionProvider + : UserSelectionProvider.getDefaultSelectionProvider(); + this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager); + } + /** * Start building a new transaction manager for the given driver instance. - * @param driver A fixed driver instance. - * @return A builder for a transaction manager + * @param driver a fixed driver instance. + * @return a builder for a transaction manager */ @API(status = API.Status.STABLE, since = "6.2") public static Builder with(Driver driver) { @@ -71,150 +138,20 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa } /** - * A builder for {@link Neo4jTransactionManager}. + * This method provides a native Neo4j transaction to be used from within a + * {@link org.springframework.data.neo4j.core.Neo4jClient}. In most cases this the + * native transaction will be controlled from the Neo4j specific + * {@link org.springframework.transaction.PlatformTransactionManager}. However, SDN + * provides support for other transaction managers as well. This method registers a + * session synchronization in such cases on the foreign transaction manager. + * @param driver the driver that has been used as a synchronization object. + * @param targetDatabase the target database + * @param asUser the user for which the tx is being retrieved + * @return an optional managed transaction or {@literal null} if the method hasn't + * been called inside an ongoing Spring transaction */ - @API(status = API.Status.STABLE, since = "6.2") - @SuppressWarnings("HiddenField") - public static final class Builder { - - private final Driver driver; - - @Nullable - private DatabaseSelectionProvider databaseSelectionProvider; - - @Nullable - private UserSelectionProvider userSelectionProvider; - - @Nullable - private Neo4jBookmarkManager bookmarkManager; - - private Builder(Driver driver) { - this.driver = driver; - } - - /** - * Configures the database selection provider. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.Neo4jClient}. During runtime, it will be checked if a call is made - * for the same database when happening in a managed transaction. - * - * @param databaseSelectionProvider The database selection provider - * @return The builder - */ - public Builder withDatabaseSelectionProvider(@Nullable DatabaseSelectionProvider databaseSelectionProvider) { - this.databaseSelectionProvider = databaseSelectionProvider; - return this; - } - - /** - * Configures a provider for impersonated users. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.Neo4jClient}. During runtime, it will be checked if a call is made - * for the same user when happening in a managed transaction. - * - * @param userSelectionProvider The provider for impersonated users - * @return The builder - */ - public Builder withUserSelectionProvider(@Nullable UserSelectionProvider userSelectionProvider) { - this.userSelectionProvider = userSelectionProvider; - return this; - } - - public Builder withBookmarkManager(Neo4jBookmarkManager bookmarkManager) { - this.bookmarkManager = bookmarkManager; - return this; - } - - public Neo4jTransactionManager build() { - return new Neo4jTransactionManager(this); - } - } - - /** - * The underlying driver, which is also the synchronisation object. - */ - private transient final Driver driver; - - /** - * Database name provider. - */ - private transient final DatabaseSelectionProvider databaseSelectionProvider; - - /** - * Provider for user impersonation. - */ - private transient final UserSelectionProvider userSelectionProvider; - - private transient final BookmarkManagerReference bookmarkManager; - - /** - * This will create a transaction manager for the default database. - * - * @param driver A driver instance - */ - public Neo4jTransactionManager(Driver driver) { - - this(with(driver)); - } - - /** - * This will create a transaction manager targeting whatever the database selection provider determines. - * - * @param driver A driver instance - * @param databaseSelectionProvider The database selection provider to determine the database in which the transactions should happen - */ - public Neo4jTransactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { - - this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider)); - } - - /** - * This constructor can be used to configure the bookmark manager being used. It is useful when you need to seed - * the bookmark manager or if you want to capture new bookmarks. - * - * @param driver A driver instance - * @param databaseSelectionProvider The database selection provider to determine the database in which the transactions should happen - * @param bookmarkManager A bookmark manager - */ - public Neo4jTransactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider, Neo4jBookmarkManager bookmarkManager) { - - this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider).withBookmarkManager(bookmarkManager)); - } - - private Neo4jTransactionManager(Builder builder) { - - this.driver = builder.driver; - this.databaseSelectionProvider = builder.databaseSelectionProvider == null ? - DatabaseSelectionProvider.getDefaultSelectionProvider() : - builder.databaseSelectionProvider; - this.userSelectionProvider = builder.userSelectionProvider == null ? - UserSelectionProvider.getDefaultSelectionProvider() : - builder.userSelectionProvider; - this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - - this.bookmarkManager.setApplicationContext(applicationContext); - } - - /** - * This method provides a native Neo4j transaction to be used from within a {@link org.springframework.data.neo4j.core.Neo4jClient}. - * In most cases this the native transaction will be controlled from the Neo4j specific {@link org.springframework.transaction.PlatformTransactionManager}. - * However, SDN provides support for other transaction managers as well. This method registers a session synchronization - * in such cases on the foreign transaction manager. - * - * @param driver The driver that has been used as a synchronization object. - * @param targetDatabase The target database - * @param asUser The user for which the tx is being retrieved - * @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing - * Spring transaction - */ - @Nullable - public static Transaction retrieveTransaction( - final Driver driver, - final DatabaseSelection targetDatabase, - final UserSelection asUser - ) { + @Nullable public static Transaction retrieveTransaction(final Driver driver, final DatabaseSelection targetDatabase, + final UserSelection asUser) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { return null; @@ -222,7 +159,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa // Check whether we have a transaction managed by a Neo4j transaction manager Neo4jTransactionHolder connectionHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager - .getResource(driver); + .getResource(driver); if (connectionHolder != null) { Transaction optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase, asUser); @@ -231,21 +168,22 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa return optionalOngoingTransaction; } - throw new IllegalStateException( - Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage( - connectionHolder.getDatabaseSelection(), targetDatabase, - connectionHolder.getUserSelection(), asUser)); + throw new IllegalStateException(Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage( + connectionHolder.getDatabaseSelection(), targetDatabase, connectionHolder.getUserSelection(), + asUser)); } // Otherwise we open a session and synchronize it. Session session = driver.session(Neo4jTransactionUtils.defaultSessionConfig(targetDatabase, asUser)); - Transaction transaction = session.beginTransaction(Neo4jTransactionUtils.createTransactionConfigFrom(TransactionDefinition.withDefaults(), -1)); + Transaction transaction = session.beginTransaction( + Neo4jTransactionUtils.createTransactionConfigFrom(TransactionDefinition.withDefaults(), -1)); // Manually create a new synchronization - connectionHolder = new Neo4jTransactionHolder(new Neo4jTransactionContext(targetDatabase, asUser), session, transaction); + connectionHolder = new Neo4jTransactionHolder(new Neo4jTransactionContext(targetDatabase, asUser), session, + transaction); connectionHolder.setSynchronizedWithTransaction(true); TransactionSynchronizationManager - .registerSynchronization(new Neo4jSessionSynchronization(connectionHolder, driver)); + .registerSynchronization(new Neo4jSessionSynchronization(connectionHolder, driver)); TransactionSynchronizationManager.bindResource(driver, connectionHolder); return Objects.requireNonNull(connectionHolder.getTransaction(targetDatabase, asUser)); @@ -265,11 +203,17 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa return extractNeo4jTransaction(status.getTransaction()); } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.bookmarkManager.setApplicationContext(applicationContext); + } + @Override protected Object doGetTransaction() throws TransactionException { Neo4jTransactionHolder resourceHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager - .getResource(driver); + .getResource(this.driver); return new Neo4jTransactionObject(resourceHolder); } @@ -283,7 +227,8 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); - TransactionConfig transactionConfig = Neo4jTransactionUtils.createTransactionConfigFrom(definition, super.getDefaultTimeout()); + TransactionConfig transactionConfig = Neo4jTransactionUtils.createTransactionConfigFrom(definition, + super.getDefaultTimeout()); boolean readOnly = definition.isReadOnly(); TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); @@ -291,11 +236,12 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa try { // Prepare configuration data Neo4jTransactionContext context = new Neo4jTransactionContext( - databaseSelectionProvider.getDatabaseSelection(), userSelectionProvider.getUserSelection(), bookmarkManager.resolve().getBookmarks()); + this.databaseSelectionProvider.getDatabaseSelection(), + this.userSelectionProvider.getUserSelection(), this.bookmarkManager.resolve().getBookmarks()); // Configure and open session together with a native transaction - Session session = this.driver.session( - Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseSelection(), context.getUserSelection())); + Session session = this.driver.session(Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), + context.getDatabaseSelection(), context.getUserSelection())); Transaction nativeTransaction = session.beginTransaction(transactionConfig); // Synchronize on that @@ -304,9 +250,10 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa transactionObject.setResourceHolder(transactionHolder); TransactionSynchronizationManager.bindResource(this.driver, transactionHolder); - } catch (Exception ex) { - throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()), - ex); + } + catch (Exception ex) { + throw new TransactionSystemException( + String.format("Could not open a new Neo4j session: %s", ex.getMessage()), ex); } } @@ -316,7 +263,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); transactionObject.setResourceHolder(null); - return TransactionSynchronizationManager.unbindResource(driver); + return TransactionSynchronizationManager.unbindResource(this.driver); } @Override @@ -325,7 +272,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa Neo4jTransactionObject transactionObject = extractNeo4jTransaction(Objects.requireNonNull(transaction)); transactionObject.setResourceHolder((Neo4jTransactionHolder) suspendedResources); - TransactionSynchronizationManager.bindResource(driver, suspendedResources); + TransactionSynchronizationManager.bindResource(this.driver, suspendedResources); } @Override @@ -336,9 +283,11 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa Neo4jTransactionHolder transactionHolder = transactionObject.getRequiredResourceHolder(); Collection newBookmarks = transactionHolder.commit(); this.bookmarkManager.resolve().updateBookmarks(transactionHolder.getBookmarks(), newBookmarks); - } catch (Neo4jException ex) { + } + catch (Neo4jException ex) { if (ex instanceof RetryableException) { - throw new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex); + throw new TransactionSystemException( + Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex); } throw ex; } @@ -364,15 +313,77 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); transactionObject.getRequiredResourceHolder().close(); transactionObject.setResourceHolder(null); - TransactionSynchronizationManager.unbindResource(driver); + TransactionSynchronizationManager.unbindResource(this.driver); + } + + /** + * A builder for {@link Neo4jTransactionManager}. + */ + @API(status = API.Status.STABLE, since = "6.2") + @SuppressWarnings("HiddenField") + public static final class Builder { + + private final Driver driver; + + @Nullable + private DatabaseSelectionProvider databaseSelectionProvider; + + @Nullable + private UserSelectionProvider userSelectionProvider; + + @Nullable + private Neo4jBookmarkManager bookmarkManager; + + private Builder(Driver driver) { + this.driver = driver; + } + + /** + * Configures the database selection provider. Make sure to use the same instance + * as for a possible {@link org.springframework.data.neo4j.core.Neo4jClient}. + * During runtime, it will be checked if a call is made for the same database when + * happening in a managed transaction. + * @param databaseSelectionProvider the database selection provider + * @return the builder + */ + public Builder withDatabaseSelectionProvider(@Nullable DatabaseSelectionProvider databaseSelectionProvider) { + this.databaseSelectionProvider = databaseSelectionProvider; + return this; + } + + /** + * Configures a provider for impersonated users. Make sure to use the same + * instance as for a possible + * {@link org.springframework.data.neo4j.core.Neo4jClient}. During runtime, it + * will be checked if a call is made for the same user when happening in a managed + * transaction. + * @param userSelectionProvider the provider for impersonated users + * @return the builder + */ + public Builder withUserSelectionProvider(@Nullable UserSelectionProvider userSelectionProvider) { + this.userSelectionProvider = userSelectionProvider; + return this; + } + + public Builder withBookmarkManager(Neo4jBookmarkManager bookmarkManager) { + this.bookmarkManager = bookmarkManager; + return this; + } + + public Neo4jTransactionManager build() { + return new Neo4jTransactionManager(this); + } + } static class Neo4jTransactionObject implements SmartTransactionObject { private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O"; - // The resource holder is null when the call to TransactionSynchronizationManager.getResource - // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. + // The resource holder is null when the call to + // TransactionSynchronizationManager.getResource + // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding + // resource holder. // If it is null, there's no existing session / transaction. @Nullable private Neo4jTransactionHolder resourceHolder; @@ -381,23 +392,23 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa this.resourceHolder = resourceHolder; } + @Nullable Neo4jTransactionHolder getResourceHolder() { + return this.resourceHolder; + } + /** - * Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's no existing - * transaction. - * - * @param resourceHolder A newly created resource holder with a fresh drivers' session, + * Usually called in {@link #doBegin(Object, TransactionDefinition)} which is + * called when there's no existing transaction. + * @param resourceHolder a newly created resource holder with a fresh drivers' + * session */ void setResourceHolder(@Nullable Neo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; } - @Nullable Neo4jTransactionHolder getResourceHolder() { - return resourceHolder; - } - Neo4jTransactionHolder getRequiredResourceHolder() { - return Objects.requireNonNull(resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); + return Objects.requireNonNull(this.resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); } void setRollbackOnly() { @@ -415,5 +426,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa TransactionSynchronizationUtils.triggerFlush(); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index b3abb74f8..e8124cddd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -25,6 +25,7 @@ import org.neo4j.driver.AccessMode; import org.neo4j.driver.Bookmark; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.TransactionConfig; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.support.UserAgent; @@ -36,23 +37,28 @@ import org.springframework.util.ReflectionUtils; /** * Internal use only. * + * @author Michael J. Simons + * @author Gerrit Meier * @since 6.0 */ public final class Neo4jTransactionUtils { @Nullable - private static final Method WITH_IMPERSONATED_USER - = ReflectionUtils.findMethod(SessionConfig.Builder.class, "withImpersonatedUser", String.class); + private static final Method WITH_IMPERSONATED_USER = ReflectionUtils.findMethod(SessionConfig.Builder.class, + "withImpersonatedUser", String.class); + + private Neo4jTransactionUtils() { + } public static boolean driverSupportsImpersonation() { return WITH_IMPERSONATED_USER != null; } - @SuppressWarnings({"UnusedReturnValue", "NullAway"}) + @SuppressWarnings({ "UnusedReturnValue", "NullAway" }) public static SessionConfig.Builder withImpersonatedUser(SessionConfig.Builder builder, String user) { if (driverSupportsImpersonation()) { - //noinspection ConstantConditions + // noinspection ConstantConditions return (SessionConfig.Builder) ReflectionUtils.invokeMethod(WITH_IMPERSONATED_USER, builder, user); } return builder; @@ -60,18 +66,19 @@ public final class Neo4jTransactionUtils { /** * The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks. - * - * @param databaseSelection The database to use. - * @param asUser An impersonated user. - * @return Session parameters to configure the default session used + * @param databaseSelection the database to use. + * @param asUser an impersonated user. + * @return aession parameters to configure the default session used */ public static SessionConfig defaultSessionConfig(DatabaseSelection databaseSelection, UserSelection asUser) { return sessionConfig(false, Collections.emptyList(), databaseSelection, asUser); } - public static SessionConfig sessionConfig(boolean readOnly, Collection bookmarks, DatabaseSelection databaseSelection, UserSelection asUser) { + public static SessionConfig sessionConfig(boolean readOnly, Collection bookmarks, + DatabaseSelection databaseSelection, UserSelection asUser) { SessionConfig.Builder builder = SessionConfig.builder() - .withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE).withBookmarks(bookmarks); + .withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE) + .withBookmarks(bookmarks); if (databaseSelection.getValue() != null) { builder.withDatabase(databaseSelection.getValue()); @@ -85,15 +92,18 @@ public final class Neo4jTransactionUtils { } /** - * Maps a Spring {@link TransactionDefinition transaction definition} to a native Neo4j driver transaction. Only the - * default isolation leven ({@link TransactionDefinition#ISOLATION_DEFAULT}) and - * {@link TransactionDefinition#PROPAGATION_REQUIRED propagation required} behaviour are supported. - * - * @param definition The transaction definition passed to a Neo4j transaction manager - * @param defaultTxManagerTimeout Default timeout from the tx manager (if available, if not, use something negative) - * @return A Neo4j native transaction configuration + * Maps a Spring {@link TransactionDefinition transaction definition} to a native + * Neo4j driver transaction. Only the default isolation leven + * ({@link TransactionDefinition#ISOLATION_DEFAULT}) and + * {@link TransactionDefinition#PROPAGATION_REQUIRED propagation required} behaviour + * are supported. + * @param definition the transaction definition passed to a Neo4j transaction manager + * @param defaultTxManagerTimeout default timeout from the tx manager (if available, + * if not, use something negative) + * @return a Neo4j native transaction configuration */ - static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition, int defaultTxManagerTimeout) { + static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition, + int defaultTxManagerTimeout) { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { throw new InvalidIsolationLevelException( @@ -110,32 +120,32 @@ public final class Neo4jTransactionUtils { TransactionConfig.Builder builder = TransactionConfig.builder(); if (definition.getTimeout() > 0) { builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout())); - } else if (defaultTxManagerTimeout > 0) { + } + else if (defaultTxManagerTimeout > 0) { builder = builder.withTimeout(Duration.ofSeconds(defaultTxManagerTimeout)); } - return builder - .withMetadata(Collections.singletonMap("app", UserAgent.INSTANCE.toString())) - .build(); + return builder.withMetadata(Collections.singletonMap("app", UserAgent.INSTANCE.toString())).build(); } - static String formatOngoingTxInAnotherDbErrorMessage( - DatabaseSelection currentDb, DatabaseSelection requestedDb, - UserSelection currentUser, UserSelection requestedUser - ) { + static String formatOngoingTxInAnotherDbErrorMessage(DatabaseSelection currentDb, DatabaseSelection requestedDb, + UserSelection currentUser, UserSelection requestedUser) { String defaultDatabase = "the default database"; String defaultUser = "the default user"; - String _currentDb = currentDb.getValue() == null ? defaultDatabase : String.format("'%s'", currentDb.getValue()); - String _requestedDb = requestedDb.getValue() == null ? defaultDatabase : String.format("'%s'", requestedDb.getValue()); + String _currentDb = (currentDb.getValue() != null) ? String.format("'%s'", currentDb.getValue()) + : defaultDatabase; + String _requestedDb = (requestedDb.getValue() != null) ? String.format("'%s'", requestedDb.getValue()) + : defaultDatabase; - String _currentUser = currentUser.getValue() == null ? defaultUser : String.format("'%s'", currentUser.getValue()); - String _requestedUser = requestedUser.getValue() == null ? defaultUser : String.format("'%s'", requestedUser.getValue()); + String _currentUser = (currentUser.getValue() != null) ? String.format("'%s'", currentUser.getValue()) + : defaultUser; + String _requestedUser = (requestedUser.getValue() != null) ? String.format("'%s'", requestedUser.getValue()) + : defaultUser; - return String.format("There is already an ongoing Spring transaction for %s of %s, but you requested %s of %s", _currentUser, _currentDb, - _requestedUser, _requestedDb); + return String.format("There is already an ongoing Spring transaction for %s of %s, but you requested %s of %s", + _currentUser, _currentDb, _requestedUser, _requestedDb); } - private Neo4jTransactionUtils() {} } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/NoopBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/NoopBookmarkManager.java index 75a65b04f..7fd33f081 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/NoopBookmarkManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/NoopBookmarkManager.java @@ -24,7 +24,6 @@ import org.neo4j.driver.Bookmark; * A bookmark manager that drops all bookmarks and never provides any bookmarks. * * @author Michael J. Simons - * @soundtrack Helge Schneider - The Last Jazz * @since 7.0 */ enum NoopBookmarkManager implements Neo4jBookmarkManager { @@ -39,4 +38,5 @@ enum NoopBookmarkManager implements Neo4jBookmarkManager { @Override public void updateBookmarks(Collection usedBookmarks, Collection newBookmarks) { } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java index 6b859baaa..aa34af3d4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveDefaultBookmarkManager.java @@ -24,6 +24,7 @@ import java.util.function.Supplier; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; + import org.springframework.context.ApplicationEventPublisher; /** @@ -44,13 +45,13 @@ final class ReactiveDefaultBookmarkManager extends AbstractBookmarkManager { private ApplicationEventPublisher applicationEventPublisher; ReactiveDefaultBookmarkManager(@Nullable Supplier> bookmarksSupplier) { - this.bookmarksSupplier = bookmarksSupplier == null ? Collections::emptySet : bookmarksSupplier; + this.bookmarksSupplier = (bookmarksSupplier != null) ? bookmarksSupplier : Collections::emptySet; } @Override public Collection getBookmarks() { synchronized (this.bookmarks) { - this.bookmarks.addAll(bookmarksSupplier.get()); + this.bookmarks.addAll(this.bookmarksSupplier.get()); return Set.copyOf(this.bookmarks); } } @@ -58,10 +59,11 @@ final class ReactiveDefaultBookmarkManager extends AbstractBookmarkManager { @Override public void updateBookmarks(Collection usedBookmarks, Collection newBookmarks) { synchronized (this.bookmarks) { - usedBookmarks.stream().filter(Objects::nonNull).forEach(bookmarks::remove); - newBookmarks.stream().filter(Objects::nonNull).forEach(bookmarks::add); - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new Neo4jBookmarksUpdatedEvent(new HashSet<>(bookmarks))); + usedBookmarks.stream().filter(Objects::nonNull).forEach(this.bookmarks::remove); + newBookmarks.stream().filter(Objects::nonNull).forEach(this.bookmarks::add); + if (this.applicationEventPublisher != null) { + this.applicationEventPublisher + .publishEvent(new Neo4jBookmarksUpdatedEvent(new HashSet<>(this.bookmarks))); } } } @@ -70,4 +72,5 @@ final class ReactiveDefaultBookmarkManager extends AbstractBookmarkManager { public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java index 2440f19c5..d04d4cba3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java @@ -15,14 +15,16 @@ */ package org.springframework.data.neo4j.core.transaction; +import org.neo4j.driver.Driver; import reactor.core.publisher.Mono; -import org.neo4j.driver.Driver; import org.springframework.transaction.reactive.ReactiveResourceSynchronization; import org.springframework.transaction.reactive.TransactionSynchronization; import org.springframework.transaction.reactive.TransactionSynchronizationManager; /** + * Neo4j specific resource synchronization. + * * @author Gerrit Meier * @author Michael J. Simons * @since 6.0 @@ -40,44 +42,29 @@ final class ReactiveNeo4jSessionSynchronization this.transactionHolder = transactionHolder; } - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#shouldReleaseBeforeCompletion() - */ @Override protected boolean shouldReleaseBeforeCompletion() { return false; } - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#processResourceAfterCommit(java.lang.Object) - */ @Override protected Mono processResourceAfterCommit(ReactiveNeo4jTransactionHolder resourceHolder) { return Mono.defer(() -> super.processResourceAfterCommit(resourceHolder).then(resourceHolder.commit())).then(); } - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#afterCompletion(int) - */ @Override public Mono afterCompletion(int status) { return Mono.defer(() -> { if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { - return transactionHolder.rollback().then(super.afterCompletion(status)); + return this.transactionHolder.rollback().then(super.afterCompletion(status)); } return super.afterCompletion(status); }); } - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#releaseResource(java.lang.Object, java.lang.Object) - */ @Override protected Mono releaseResource(ReactiveNeo4jTransactionHolder resourceHolder, Object resourceKey) { return Mono.defer(() -> Mono.fromDirect(resourceHolder.getSession().close()).then()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java index 3d3de85ca..2229d318e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.transaction; -import reactor.core.publisher.Mono; - import java.util.Collection; import java.util.Set; @@ -24,11 +22,16 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Bookmark; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Mono; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.transaction.support.ResourceHolderSupport; /** + * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a + * {@link org.neo4j.driver.reactive.ReactiveTransaction}. + * * @author Gerrit Meier * @author Michael J. Simons * @since 6.0 @@ -36,10 +39,13 @@ import org.springframework.transaction.support.ResourceHolderSupport; final class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { private final Neo4jTransactionContext context; + private final ReactiveSession session; + private final ReactiveTransaction transaction; - ReactiveNeo4jTransactionHolder(Neo4jTransactionContext context, ReactiveSession session, ReactiveTransaction transaction) { + ReactiveNeo4jTransactionHolder(Neo4jTransactionContext context, ReactiveSession session, + ReactiveTransaction transaction) { this.context = context; this.session = session; @@ -47,36 +53,36 @@ final class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { } ReactiveSession getSession() { - return session; + return this.session; } - @Nullable - ReactiveTransaction getTransaction(DatabaseSelection inDatabase, UserSelection asUser) { + @Nullable ReactiveTransaction getTransaction(DatabaseSelection inDatabase, UserSelection asUser) { - return this.context.isForDatabaseAndUser(inDatabase, asUser) ? transaction : null; + return this.context.isForDatabaseAndUser(inDatabase, asUser) ? this.transaction : null; } Mono> commit() { - return Mono.fromDirect(transaction.commit()).then(Mono.fromSupplier(session::lastBookmarks)); + return Mono.fromDirect(this.transaction.commit()).then(Mono.fromSupplier(this.session::lastBookmarks)); } Mono rollback() { - return Mono.fromDirect(transaction.rollback()).then(); + return Mono.fromDirect(this.transaction.rollback()).then(); } Mono close() { - return Mono.fromDirect(session.close()).then(); + return Mono.fromDirect(this.session.close()).then(); } DatabaseSelection getDatabaseSelection() { - return context.getDatabaseSelection(); + return this.context.getDatabaseSelection(); } UserSelection getUserSelection() { - return context.getUserSelection(); + return this.context.getUserSelection(); } Collection getBookmarks() { - return context.getBookmarks(); + return this.context.getBookmarks(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java index ded60db71..d347b3cf0 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java @@ -18,9 +18,6 @@ package org.springframework.data.neo4j.core.transaction; import java.io.Serial; import java.util.Objects; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuples; - import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Driver; @@ -28,6 +25,9 @@ import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.exceptions.RetryableException; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -48,20 +48,88 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.util.Assert; /** + * Neo4j specific implementation of an {@link AbstractReactiveTransactionManager}. + * * @author Gerrit Meier * @author Michael J. Simons * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") -public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager implements ApplicationContextAware { +public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager + implements ApplicationContextAware { @Serial private static final long serialVersionUID = 204661696798919944L; + /** + * The underlying driver, which is also the synchronisation object. + */ + private final transient Driver driver; + + /** + * Database name provider. + */ + private final transient ReactiveDatabaseSelectionProvider databaseSelectionProvider; + + /** + * Provider for user impersonation. + */ + private final transient ReactiveUserSelectionProvider userSelectionProvider; + + private final transient BookmarkManagerReference bookmarkManager; + + /** + * This will create a transaction manager for the default database. + * @param driver a driver instance + */ + public ReactiveNeo4jTransactionManager(Driver driver) { + + this(with(driver)); + } + + /** + * This will create a transaction manager targeting whatever the database selection + * provider determines. + * @param driver a driver instance + * @param databaseSelectionProvider the database selection provider to determine the + * database in which the transactions should happen + */ + public ReactiveNeo4jTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider)); + } + + /** + * This constructor can be used to configure the bookmark manager being used. It is + * useful when you need to seed the bookmark manager or if you want to capture new + * bookmarks. + * @param driver a driver instance + * @param databaseSelectionProvider the database selection provider to determine the + * database in which the transactions should happen + * @param bookmarkManager a bookmark manager + */ + public ReactiveNeo4jTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider, + Neo4jBookmarkManager bookmarkManager) { + + this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider) + .withBookmarkManager(bookmarkManager)); + } + + private ReactiveNeo4jTransactionManager(Builder builder) { + + this.driver = builder.driver; + this.databaseSelectionProvider = (builder.databaseSelectionProvider != null) ? builder.databaseSelectionProvider + : ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider(); + this.userSelectionProvider = (builder.userSelectionProvider != null) ? builder.userSelectionProvider + : ReactiveUserSelectionProvider.getDefaultSelectionProvider(); + this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, + builder.bookmarkManager); + } + /** * Start building a new transaction manager for the given driver instance. - * @param driver A fixed driver instance. - * @return A builder for a transaction manager + * @param driver a fixed driver instance. + * @return a builder for a transaction manager */ @API(status = API.Status.STABLE, since = "6.2") public static Builder with(Driver driver) { @@ -69,6 +137,201 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans return new Builder(driver); } + /** + * Retrieves a new transaction. + * @param driver the driver that has been used as a synchronization object. + * @param targetDatabase the target database + * @param asUser the target user + * @return an optional managed transaction or {@literal null} if the method hasn't + * been called inside an ongoing Spring transaction + */ + public static Mono retrieveReactiveTransaction(final Driver driver, + final DatabaseSelection targetDatabase, final UserSelection asUser) { + + // Do we have a Transaction context? Bail out early if synchronization between + // transaction managers is not active. + return TransactionSynchronizationManager.forCurrentTransaction() + .filter(TransactionSynchronizationManager::isSynchronizationActive) + .flatMap(tsm -> { + // Get an existing holder + ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm + .getResource(driver); + + // And use it if there is any + if (existingTxHolder != null) { + return Mono.just(existingTxHolder); + } + + // Otherwise open up a new native transaction + return Mono.defer(() -> { + + ReactiveSession session = driver.session(ReactiveSession.class, + Neo4jTransactionUtils.defaultSessionConfig(targetDatabase, asUser)); + return Mono.fromDirect(session.beginTransaction(Neo4jTransactionUtils + .createTransactionConfigFrom(TransactionDefinition.withDefaults(), -1))).map(tx -> { + + ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder( + new Neo4jTransactionContext(targetDatabase, asUser), session, tx); + newConnectionHolder.setSynchronizedWithTransaction(true); + + tsm.registerSynchronization( + new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver)); + + tsm.bindResource(driver, newConnectionHolder); + return newConnectionHolder; + }); + }); + }) + .handle((connectionHolder, sink) -> { + ReactiveTransaction transaction = connectionHolder.getTransaction(targetDatabase, asUser); + if (transaction == null) { + sink.error(new IllegalStateException(Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage( + connectionHolder.getDatabaseSelection(), targetDatabase, + connectionHolder.getUserSelection(), asUser))); + return; + } + sink.next(transaction); + }) + // If not, then just don't open a transaction + .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); + } + + private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(Object transaction) { + + Assert.isInstanceOf(ReactiveNeo4jTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s", + ReactiveNeo4jTransactionObject.class, transaction.getClass())); + + return (ReactiveNeo4jTransactionObject) transaction; + } + + private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(GenericReactiveTransaction status) { + return extractNeo4jTransaction(status.getTransaction()); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.bookmarkManager.setApplicationContext(applicationContext); + } + + @Override + protected Object doGetTransaction(TransactionSynchronizationManager transactionSynchronizationManager) + throws TransactionException { + + ReactiveNeo4jTransactionHolder resourceHolder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager + .getResource(this.driver); + return new ReactiveNeo4jTransactionObject(resourceHolder); + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + + return extractNeo4jTransaction(transaction).getResourceHolder() != null; + } + + @Override + protected Mono doBegin(TransactionSynchronizationManager transactionSynchronizationManager, + Object transaction, TransactionDefinition transactionDefinition) throws TransactionException { + + return Mono.defer(() -> { + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + + TransactionConfig transactionConfig = Neo4jTransactionUtils + .createTransactionConfigFrom(transactionDefinition, -1); + boolean readOnly = transactionDefinition.isReadOnly(); + + transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); + + return this.databaseSelectionProvider.getDatabaseSelection() + .switchIfEmpty(Mono.just(DatabaseSelection.undecided())) + .zipWith( + this.userSelectionProvider.getUserSelection() + .switchIfEmpty(Mono.just(UserSelection.connectedUser())), + (databaseSelection, userSelection) -> new Neo4jTransactionContext(databaseSelection, + userSelection, this.bookmarkManager.resolve().getBookmarks())) + .map(context -> Tuples.of(context, + this.driver.session(ReactiveSession.class, + Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), + context.getDatabaseSelection(), context.getUserSelection())))) + .flatMap(contextAndSession -> Mono + .fromDirect(contextAndSession.getT2().beginTransaction(transactionConfig)) + .single() + .map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(), + contextAndSession.getT2(), nativeTransaction))) + .doOnNext(transactionHolder -> { + transactionHolder.setSynchronizedWithTransaction(true); + transactionObject.setResourceHolder(transactionHolder); + transactionSynchronizationManager.bindResource(this.driver, transactionHolder); + }); + + }).then(); + } + + @Override + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager, + Object transaction) { + + return Mono.just(extractNeo4jTransaction(transaction)).map(r -> { + ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder(); + r.setResourceHolder(null); + return holder; + }) + .flatMap(ReactiveNeo4jTransactionHolder::close) + .then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(this.driver))); + } + + @Override + protected Mono doCommit(TransactionSynchronizationManager transactionSynchronizationManager, + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { + + ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) + .getRequiredResourceHolder(); + return holder.commit() + .doOnNext(bookmark -> this.bookmarkManager.resolve().updateBookmarks(holder.getBookmarks(), bookmark)) + .onErrorMap(e -> e instanceof RetryableException, + ex -> new TransactionSystemException( + Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex)) + .then(); + } + + @Override + protected Mono doRollback(TransactionSynchronizationManager transactionSynchronizationManager, + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { + + ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) + .getRequiredResourceHolder(); + return holder.rollback(); + } + + @Override + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) + throws TransactionException { + + return Mono.just(extractNeo4jTransaction(transaction)) + .doOnNext(r -> r.setResourceHolder(null)) + .then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(this.driver))); + } + + @Override + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, + @Nullable Object transaction, Object suspendedResources) throws TransactionException { + + return Mono.just(extractNeo4jTransaction(Objects.requireNonNull(transaction))) + .doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources)) + .then(Mono.fromRunnable(() -> synchronizationManager.bindResource(this.driver, suspendedResources))); + } + + @Override + protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { + + return Mono.fromRunnable(() -> { + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(genericReactiveTransaction); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + }); + } + /** * A builder for {@link ReactiveNeo4jTransactionManager}. */ @@ -92,25 +355,28 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans } /** - * Configures the database selection provider. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.ReactiveNeo4jClient}. During runtime, it will be checked if a call is made - * for the same database when happening in a managed transaction. - * - * @param databaseSelectionProvider The database selection provider - * @return The builder + * Configures the database selection provider. Make sure to use the same instance + * as for a possible + * {@link org.springframework.data.neo4j.core.ReactiveNeo4jClient}. During + * runtime, it will be checked if a call is made for the same database when + * happening in a managed transaction. + * @param databaseSelectionProvider the database selection provider + * @return the builder */ - public Builder withDatabaseSelectionProvider(@Nullable ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public Builder withDatabaseSelectionProvider( + @Nullable ReactiveDatabaseSelectionProvider databaseSelectionProvider) { this.databaseSelectionProvider = databaseSelectionProvider; return this; } /** - * Configures a provider for impersonated users. Make sure to use the same instance as for a possible - * {@link org.springframework.data.neo4j.core.ReactiveNeo4jClient}. During runtime, it will be checked if a call is made - * for the same user when happening in a managed transaction. - * - * @param userSelectionProvider The provider for impersonated users - * @return The builder + * Configures a provider for impersonated users. Make sure to use the same + * instance as for a possible + * {@link org.springframework.data.neo4j.core.ReactiveNeo4jClient}. During + * runtime, it will be checked if a call is made for the same user when happening + * in a managed transaction. + * @param userSelectionProvider the provider for impersonated users + * @return the builder */ public Builder withUserSelectionProvider(@Nullable ReactiveUserSelectionProvider userSelectionProvider) { this.userSelectionProvider = userSelectionProvider; @@ -125,265 +391,17 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans public ReactiveNeo4jTransactionManager build() { return new ReactiveNeo4jTransactionManager(this); } - } - /** - * The underlying driver, which is also the synchronisation object. - */ - private transient final Driver driver; - - /** - * Database name provider. - */ - private transient final ReactiveDatabaseSelectionProvider databaseSelectionProvider; - - /** - * Provider for user impersonation. - */ - private transient final ReactiveUserSelectionProvider userSelectionProvider; - - private transient final BookmarkManagerReference bookmarkManager; - - /** - * This will create a transaction manager for the default database. - * - * @param driver A driver instance - */ - public ReactiveNeo4jTransactionManager(Driver driver) { - - this(with(driver)); - } - - /** - * This will create a transaction manager targeting whatever the database selection provider determines. - * - * @param driver A driver instance - * @param databaseSelectionProvider The database selection provider to determine the database in which the transactions should happen - */ - public ReactiveNeo4jTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider)); - } - - /** - * This constructor can be used to configure the bookmark manager being used. It is useful when you need to seed - * the bookmark manager or if you want to capture new bookmarks. - * - * @param driver A driver instance - * @param databaseSelectionProvider The database selection provider to determine the database in which the transactions should happen - * @param bookmarkManager A bookmark manager - */ - public ReactiveNeo4jTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider, Neo4jBookmarkManager bookmarkManager) { - - this(with(driver).withDatabaseSelectionProvider(databaseSelectionProvider).withBookmarkManager(bookmarkManager)); - } - - private ReactiveNeo4jTransactionManager(Builder builder) { - - this.driver = builder.driver; - this.databaseSelectionProvider = builder.databaseSelectionProvider == null ? - ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider() : - builder.databaseSelectionProvider; - this.userSelectionProvider = builder.userSelectionProvider == null ? - ReactiveUserSelectionProvider.getDefaultSelectionProvider() : - builder.userSelectionProvider; - this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, builder.bookmarkManager); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - - this.bookmarkManager.setApplicationContext(applicationContext); - } - - /** - * @param driver The driver that has been used as a synchronization object. - * @param targetDatabase The target database - * @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing - * Spring transaction - */ - public static Mono retrieveReactiveTransaction( - final Driver driver, - final DatabaseSelection targetDatabase, - final UserSelection asUser - ) { - - return TransactionSynchronizationManager.forCurrentTransaction() // Do we have a Transaction context? - // Bail out early if synchronization between transaction managers is not active - .filter(TransactionSynchronizationManager::isSynchronizationActive).flatMap(tsm -> { - // Get an existing holder - ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm.getResource(driver); - - // And use it if there is any - if (existingTxHolder != null) { - return Mono.just(existingTxHolder); - } - - // Otherwise open up a new native transaction - return Mono.defer(() -> { - - ReactiveSession session = driver.session(ReactiveSession.class, Neo4jTransactionUtils.defaultSessionConfig(targetDatabase, asUser)); - return Mono.fromDirect(session.beginTransaction(Neo4jTransactionUtils.createTransactionConfigFrom(TransactionDefinition.withDefaults(), -1))).map(tx -> { - - ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder( - new Neo4jTransactionContext(targetDatabase, asUser), session, tx); - newConnectionHolder.setSynchronizedWithTransaction(true); - - tsm.registerSynchronization(new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver)); - - tsm.bindResource(driver, newConnectionHolder); - return newConnectionHolder; - }); - }); - }).map(connectionHolder -> { - ReactiveTransaction transaction = connectionHolder.getTransaction(targetDatabase, asUser); - if (transaction == null) { - throw new IllegalStateException( - Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage( - connectionHolder.getDatabaseSelection(), targetDatabase, - connectionHolder.getUserSelection(), asUser)); - } - return transaction; - }) - // If not, then just don't open a transaction - .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); - } - - private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(Object transaction) { - - Assert.isInstanceOf(ReactiveNeo4jTransactionObject.class, transaction, - () -> String.format("Expected to find a %s but it turned out to be %s", ReactiveNeo4jTransactionObject.class, - transaction.getClass())); - - return (ReactiveNeo4jTransactionObject) transaction; - } - - private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(GenericReactiveTransaction status) { - return extractNeo4jTransaction(status.getTransaction()); - } - - @Override - protected Object doGetTransaction(TransactionSynchronizationManager transactionSynchronizationManager) - throws TransactionException { - - ReactiveNeo4jTransactionHolder resourceHolder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager - .getResource(driver); - return new ReactiveNeo4jTransactionObject(resourceHolder); - } - - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(Object) - */ - @Override - protected boolean isExistingTransaction(Object transaction) throws TransactionException { - - return extractNeo4jTransaction(transaction).getResourceHolder() != null; - } - - @Override - protected Mono doBegin(TransactionSynchronizationManager transactionSynchronizationManager, Object transaction, - TransactionDefinition transactionDefinition) throws TransactionException { - - return Mono.defer(() -> { - ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); - - TransactionConfig transactionConfig = Neo4jTransactionUtils.createTransactionConfigFrom(transactionDefinition, -1); - boolean readOnly = transactionDefinition.isReadOnly(); - - transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); - - return databaseSelectionProvider - .getDatabaseSelection() - .switchIfEmpty(Mono.just(DatabaseSelection.undecided())) - .zipWith( - userSelectionProvider - .getUserSelection() - .switchIfEmpty(Mono.just(UserSelection.connectedUser())), - (databaseSelection, userSelection) -> new Neo4jTransactionContext(databaseSelection, userSelection, bookmarkManager.resolve().getBookmarks())) - .map(context -> Tuples.of(context, this.driver.session(ReactiveSession.class, Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseSelection(), context.getUserSelection())))) - .flatMap(contextAndSession -> Mono.fromDirect(contextAndSession.getT2().beginTransaction(transactionConfig)).single() - .map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(), - contextAndSession.getT2(), nativeTransaction))) - .doOnNext(transactionHolder -> { - transactionHolder.setSynchronizedWithTransaction(true); - transactionObject.setResourceHolder(transactionHolder); - transactionSynchronizationManager.bindResource(this.driver, transactionHolder); - }); - - }).then(); - } - - @Override - protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager, - Object transaction) { - - return Mono.just(extractNeo4jTransaction(transaction)).map(r -> { - ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder(); - r.setResourceHolder(null); - return holder; - }).flatMap(ReactiveNeo4jTransactionHolder::close) - .then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(driver))); - } - - @Override - protected Mono doCommit(TransactionSynchronizationManager transactionSynchronizationManager, - GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { - - ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) - .getRequiredResourceHolder(); - return holder.commit() - .doOnNext(bookmark -> bookmarkManager.resolve().updateBookmarks(holder.getBookmarks(), bookmark)) - .onErrorMap(e -> e instanceof RetryableException, ex -> new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex)) - .then(); - } - - @Override - protected Mono doRollback(TransactionSynchronizationManager transactionSynchronizationManager, - GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { - - ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) - .getRequiredResourceHolder(); - return holder.rollback(); - } - - @Override - protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) - throws TransactionException { - - return Mono.just(extractNeo4jTransaction(transaction)).doOnNext(r -> r.setResourceHolder(null)) - .then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(driver))); - } - - @Override - protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction, - Object suspendedResources) throws TransactionException { - - return Mono.just(extractNeo4jTransaction(Objects.requireNonNull(transaction))) - .doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources)) - .then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources))); - } - - /* - * (non-Javadoc) - * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) - */ - @Override - protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { - - return Mono.fromRunnable(() -> { - ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(genericReactiveTransaction); - transactionObject.getRequiredResourceHolder().setRollbackOnly(); - }); } static class ReactiveNeo4jTransactionObject implements SmartTransactionObject { private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O"; - // The resource holder is null when the call to TransactionSynchronizationManager.getResource - // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. + // The resource holder is null when the call to + // TransactionSynchronizationManager.getResource + // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding + // resource holder. // If it is null, there's no existing session / transaction. @Nullable private ReactiveNeo4jTransactionHolder resourceHolder; @@ -392,23 +410,24 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans this.resourceHolder = resourceHolder; } - /** - * Usually called in {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} which is - * called when there's no existing transaction. - * - * @param resourceHolder A newly created resource holder with a fresh drivers' session, - */ - void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { - this.resourceHolder = resourceHolder; - } - ReactiveNeo4jTransactionHolder getRequiredResourceHolder() { return Objects.requireNonNull(this.resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); } @Nullable ReactiveNeo4jTransactionHolder getResourceHolder() { - return resourceHolder; + return this.resourceHolder; + } + + /** + * Usually called in + * {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} + * which is called when there's no existing transaction. + * @param resourceHolder a newly created resource holder with a fresh drivers' + * session, + */ + void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { + this.resourceHolder = resourceHolder; } @Override @@ -421,6 +440,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans TransactionSynchronizationUtils.triggerFlush(); } + } } diff --git a/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java b/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java index 800ad44b5..6a84c70ad 100644 --- a/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java @@ -1,8 +1,23 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - Contains the core infrastructure for translating unmanaged Neo4j transaction into Spring managed transactions. Exposes - both the imperative and reactive `TransactionManager` as `Neo4jTransactionManager` and `ReactiveNeo4jTransactionManager`. - * + * Contains the core infrastructure for translating unmanaged Neo4j + * transaction into Spring managed transactions. Exposes both the imperative and reactive + * `TransactionManager` as `Neo4jTransactionManager` and + * `ReactiveNeo4jTransactionManager`. */ @NullMarked package org.springframework.data.neo4j.core.transaction; diff --git a/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java index fdcce206a..3a4969c1e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java @@ -27,55 +27,32 @@ import org.springframework.data.repository.query.QueryByExampleExecutor; /** * Neo4j specific {@link org.springframework.data.repository.Repository} interface. * - * @author Michael J. Simons - * @author Ján Šúr * @param type of the domain class to map * @param identifier type in the domain class + * @author Michael J. Simons + * @author Ján Šúr * @since 6.0 */ @NoRepositoryBean -public interface Neo4jRepository extends PagingAndSortingRepository, QueryByExampleExecutor, - CrudRepository { +public interface Neo4jRepository + extends PagingAndSortingRepository, QueryByExampleExecutor, CrudRepository { - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#saveAll(java.lang.Iterable) - */ @Override List saveAll(Iterable entities); - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAll() - */ @Override List findAll(); - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAllById(java.lang.Iterable) - */ @Override List findAllById(Iterable iterable); - /* - * (non-Javadoc) - * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) - */ @Override List findAll(Sort sort); - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) - */ @Override List findAll(Example example); - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) - */ @Override List findAll(Example example, Sort sort); + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java b/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java index 82dc6e350..a2317f07b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java +++ b/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java @@ -18,13 +18,13 @@ package org.springframework.data.neo4j.repository; import java.io.Serial; import org.apiguardian.api.API; + import org.springframework.dao.EmptyResultDataAccessException; /** * Throw when a query doesn't return a required result. * * @author Michael J. Simons - * @soundtrack Deichkind - Niveau weshalb warum * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -32,6 +32,7 @@ public final class NoResultException extends EmptyResultDataAccessException { @Serial private static final long serialVersionUID = -1508370436250180391L; + private final String query; public NoResultException(int expectedNumberOfResults, String query) { @@ -40,6 +41,7 @@ public final class NoResultException extends EmptyResultDataAccessException { } public String getQuery() { - return query; + return this.query; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java index 10b2d2be0..88ba7ff22 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java @@ -21,13 +21,16 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.data.repository.reactive.ReactiveSortingRepository; /** - * Neo4j specific {@link org.springframework.data.repository.Repository} interface with reactive support. + * Neo4j specific {@link org.springframework.data.repository.Repository} interface with + * reactive support. * - * @author Michael J. Simons * @param type of the domain class to map * @param identifier type in the domain class + * @author Michael J. Simons * @since 6.0 */ @NoRepositoryBean public interface ReactiveNeo4jRepository - extends ReactiveSortingRepository, ReactiveQueryByExampleExecutor, ReactiveCrudRepository {} + extends ReactiveSortingRepository, ReactiveQueryByExampleExecutor, ReactiveCrudRepository { + +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index eece6e9a5..0543aef58 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -33,9 +33,9 @@ import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryB import org.springframework.data.repository.config.DefaultRepositoryBaseClass; /** - * Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()}, - * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated - * configuration class. + * Annotation to activate Neo4j repositories. If no base package is configured through + * either {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it + * will trigger scanning of the package of annotated configuration class. * * @author Gerrit Meier * @since 6.0 @@ -48,82 +48,104 @@ import org.springframework.data.repository.config.DefaultRepositoryBaseClass; public @interface EnableNeo4jRepositories { /** - * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableNeo4jRepositories("org.my.pkg")} instead of + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @EnableNeo4jRepositories("org.my.pkg")} instead of * {@code @EnableNeo4jRepositories(basePackages="org.my.pkg")}. + * @return alias for {@link #basePackages()} */ @AliasFor("basePackages") String[] value() default {}; /** - * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this - * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * Base packages to scan for annotated components. {@link #value()} is an alias for + * (and mutually exclusive with) this attribute. Use {@link #basePackageClasses()} for + * a type-safe alternative to String-based package names. + * @return the base packages to scan */ @AliasFor("value") String[] basePackages() default {}; /** - * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The - * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in - * each package that serves no purpose other than being referenced by this attribute. + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return the base packages to scan */ Class[] basePackageClasses() default {}; /** - * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to - * {@link Neo4jRepositoryFactoryBean}. + * Returns the {@link FactoryBean} class to be used for each repository instance. + * Defaults to {@link Neo4jRepositoryFactoryBean}. + * @return the repository factory bean class */ Class repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class; /** - * Configure the repository base class to be used to create repository proxies for this particular configuration. - * + * Configure the repository base class to be used to create repository proxies for + * this particular configuration. * @return The base class to be used when creating repository proxies. */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** - * Configures the name of the {@link Neo4jMappingContext} bean to be used with the repositories detected. + * Configures the name of the {@link Neo4jMappingContext} bean to be used with the + * repositories detected. + * @return reference to the {@link Neo4jMappingContext} */ String neo4jMappingContextRef() default Neo4jRepositoryConfigurationExtension.DEFAULT_MAPPING_CONTEXT_BEAN_NAME; /** - * Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected. + * Configures the name of the {@link Neo4jTemplate} bean to be used with the + * repositories detected. + * @return reference to the {@link Neo4jTemplate} */ String neo4jTemplateRef() default Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME; /** - * Configures the name of the {@link Neo4jTransactionManager} bean to be used with the repositories detected. + * Configures the name of the {@link Neo4jTransactionManager} bean to be used with the + * repositories detected. + * @return reference to the {@link Neo4jTransactionManager} */ String transactionManagerRef() default Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; /** - * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from - * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + * Specifies which types are eligible for component scanning. Further narrows the set + * of candidate components from everything in {@link #basePackages()} to everything in + * the base packages that matches the given filter or filters. + * @return filters for components that should be included while scanning for + * repositories and entities */ ComponentScan.Filter[] includeFilters() default {}; /** * Specifies which types are not eligible for component scanning. + * @return filters for components that should be excluded while scanning for + * repositories and entities */ ComponentScan.Filter[] excludeFilters() default {}; /** - * Configures the location of where to find the Spring Data named queries properties file. Will default to - * {@code META-INFO/neo4j-named-queries.properties}. + * Configures the location of where to find the Spring Data named queries properties + * file. Will default to {@code META-INFO/neo4j-named-queries.properties}. + * @return location of a resource containing named queries */ String namedQueriesLocation() default ""; /** - * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So - * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning - * for {@code PersonRepositoryImpl}. + * Returns the postfix to be used when looking up custom repository implementations. + * Defaults to {@literal Impl}. So for a repository named {@code PersonRepository} the + * corresponding implementation class will be looked up scanning for + * {@code PersonRepositoryImpl}. + * @return postfix of specific repository implementations */ String repositoryImplementationPostfix() default "Impl"; /** - * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the - * repositories infrastructure. + * Configures whether nested repository-interfaces (e.g. defined as inner classes) + * should be discovered by the repositories' infrastructure. + * @return flag if nested repositories should be considered */ boolean considerNestedRepositories() default false; + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java b/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java index 3fbf25d27..77ee0f19b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java @@ -33,8 +33,9 @@ import org.springframework.data.neo4j.repository.support.ReactiveNeo4jRepository import org.springframework.data.repository.config.DefaultRepositoryBaseClass; /** - * Annotation to activate reactive Neo4j repositories. If no base package is configured through either {@link #value()}, - * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated + * Annotation to activate reactive Neo4j repositories. If no base package is configured + * through either {@link #value()}, {@link #basePackages()} or + * {@link #basePackageClasses()} it will trigger scanning of the package of annotated * configuration class. * * @author Gerrit Meier @@ -49,82 +50,104 @@ import org.springframework.data.repository.config.DefaultRepositoryBaseClass; public @interface EnableReactiveNeo4jRepositories { /** - * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableReactiveNeo4jRepositories("org.my.pkg")} instead of - * {@code @EnableReactiveNeo4jRepositories(basePackages="org.my.pkg")}. + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @EnableReactiveNeo4jRepositories("org.my.pkg")} instead + * of {@code @EnableReactiveNeo4jRepositories(basePackages="org.my.pkg")}. + * @return alias for {@link #basePackages()} */ @AliasFor("basePackages") String[] value() default {}; /** - * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this - * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * Base packages to scan for annotated components. {@link #value()} is an alias for + * (and mutually exclusive with) this attribute. Use {@link #basePackageClasses()} for + * a type-safe alternative to String-based package names. + * @return the base packages to scan */ @AliasFor("value") String[] basePackages() default {}; /** - * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The - * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in - * each package that serves no purpose other than being referenced by this attribute. + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return the base packages to scan */ Class[] basePackageClasses() default {}; /** - * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to - * {@link ReactiveNeo4jRepositoryFactoryBean}. + * Returns the {@link FactoryBean} class to be used for each repository instance. + * Defaults to {@link ReactiveNeo4jRepositoryFactoryBean}. + * @return the repository factory bean class */ Class repositoryFactoryBeanClass() default ReactiveNeo4jRepositoryFactoryBean.class; /** - * Configure the repository base class to be used to create repository proxies for this particular configuration. - * + * Configure the repository base class to be used to create repository proxies for + * this particular configuration. * @return The base class to be used when creating repository proxies. */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** - * Configures the name of the {@link Neo4jMappingContext} bean to be used with the repositories detected. + * Configures the name of the {@link Neo4jMappingContext} bean to be used with the + * repositories detected. + * @return reference to the {@link Neo4jMappingContext} */ String neo4jMappingContextRef() default ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_MAPPING_CONTEXT_BEAN_NAME; /** - * Configures the name of the {@link ReactiveNeo4jTemplate} bean to be used with the repositories detected. + * Configures the name of the {@link ReactiveNeo4jTemplate} bean to be used with the + * repositories detected. + * @return reference to the {@link ReactiveNeo4jTemplate} */ String neo4jTemplateRef() default ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME; /** - * Configures the name of the {@link ReactiveNeo4jTransactionManager} bean to be used with the repositories detected. + * Configures the name of the {@link ReactiveNeo4jTransactionManager} bean to be used + * with the repositories detected. + * @return reference to the {@link ReactiveNeo4jTransactionManager} */ String transactionManagerRef() default ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; /** - * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from - * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + * Specifies which types are eligible for component scanning. Further narrows the set + * of candidate components from everything in {@link #basePackages()} to everything in + * the base packages that matches the given filter or filters. + * @return filters for components that should be included while scanning for + * repositories and entities */ ComponentScan.Filter[] includeFilters() default {}; /** * Specifies which types are not eligible for component scanning. + * @return filters for components that should be excluded while scanning for + * repositories and entities */ ComponentScan.Filter[] excludeFilters() default {}; /** - * Configures the location of where to find the Spring Data named queries properties file. Will default to - * {@code META-INFO/neo4j-named-queries.properties}. + * Configures the location of where to find the Spring Data named queries properties + * file. Will default to {@code META-INFO/neo4j-named-queries.properties}. + * @return location of a resource containing named queries */ String namedQueriesLocation() default ""; /** - * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So - * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning - * for {@code PersonRepositoryImpl}. + * Returns the postfix to be used when looking up custom repository implementations. + * Defaults to {@literal Impl}. So for a repository named {@code PersonRepository} the + * corresponding implementation class will be looked up scanning for + * {@code PersonRepositoryImpl}. + * @return postfix of specific repository implementations */ String repositoryImplementationPostfix() default "Impl"; /** - * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the - * repositories infrastructure. + * Configures whether nested repository-interfaces (e.g. defined as inner classes) + * should be discovered by the repositories' infrastructure. + * @return flag if nested repositories should be considered */ boolean considerNestedRepositories() default false; + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java index dc68e06e8..38bcfec30 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java @@ -21,10 +21,11 @@ import org.springframework.data.repository.config.RepositoryBeanDefinitionRegist import org.springframework.data.repository.config.RepositoryConfigurationExtension; /** - * {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableNeo4jRepositories} annotation. The - * {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's - * {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans - * during processing of configuration classes. + * {@link RepositoryBeanDefinitionRegistrarSupport} to enable + * {@link EnableNeo4jRepositories} annotation. The + * {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of + * Spring's {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, + * a dedicated SPI to register beans during processing of configuration classes. * * @author Gerrit Meier * @author Michael J. Simons @@ -32,21 +33,14 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi */ class Neo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() - */ @Override protected Class getAnnotation() { return EnableNeo4jRepositories.class; } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() - */ @Override protected RepositoryConfigurationExtension getExtension() { return new Neo4jRepositoryConfigurationExtension(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index 38855006d..fd608c347 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; import org.apiguardian.api.API; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -35,10 +36,13 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; /** - * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide - * all necessary beans to be registered in the application's context before the user's "business" beans gets registered. + * This dedicated Neo4j repository extension will be registered via + * {@link Neo4jRepositoriesRegistrar} and then provide all necessary beans to be + * registered in the application's context before the user's "business" beans gets + * registered. *

- * While it is public, it is mainly used for internal API respectively for Spring Boots automatic configuration. + * While it is public, it is mainly used for internal API respectively for Spring Boots + * automatic configuration. * * @author Michael J. Simons * @author Gerrit Meier @@ -47,17 +51,24 @@ import org.springframework.data.repository.core.RepositoryMetadata; @API(status = API.Status.INTERNAL, since = "6.0") public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { - static final String MODULE_NAME = "Neo4j"; - static final String MODULE_PREFIX_ERROR_MSG = "This method has been deprecated and should not have been called"; - /** * See {@link AbstractBeanDefinition#INFER_METHOD}. */ public static final String DEFAULT_NEO4J_CLIENT_BEAN_NAME = "neo4jClient"; + /** + * The default name under which SDN expects a + * {@link org.springframework.data.neo4j.core.Neo4jTemplate}. + */ public static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "neo4jTemplate"; + /** + * The default name under which SDN expects a + * {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}. + */ public static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + static final String MODULE_NAME = "Neo4j"; + static final String MODULE_PREFIX_ERROR_MSG = "This method has been deprecated and should not have been called"; /** * See {@link AbstractBeanDefinition#INFER_METHOD}. @@ -69,10 +80,6 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi new StartupLogger(StartupLogger.Mode.IMPERATIVE).logStarting(); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() - */ @Override public String getRepositoryFactoryBeanClassName() { return Neo4jRepositoryFactoryBean.class.getName(); @@ -95,19 +102,18 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi } @Override - public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { - // configurationSource.getSource() might be null and registerIfNotAlreadyRegistered is non-null api, + // configurationSource.getSource() might be null and + // registerIfNotAlreadyRegistered is non-null api, // but BeanMetadataAttributeAccessor will be eventually happy with a null value // noinspection ConstantConditions - registerIfNotAlreadyRegistered(() -> BeanDefinitionBuilder - .rootBeanDefinition(Neo4jEvaluationContextExtension.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .getBeanDefinition(), - registry, - Neo4jEvaluationContextExtension.class.getName(), - configurationSource.getSource() - ); + registerIfNotAlreadyRegistered( + () -> BeanDefinitionBuilder.rootBeanDefinition(Neo4jEvaluationContextExtension.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .getBeanDefinition(), + registry, Neo4jEvaluationContextExtension.class.getName(), configurationSource.getSource()); } @Override @@ -121,10 +127,6 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi return !metadata.isReactiveRepository(); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) - */ @Override public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java index 42770667c..8d559815b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java @@ -21,10 +21,11 @@ import org.springframework.data.repository.config.RepositoryBeanDefinitionRegist import org.springframework.data.repository.config.RepositoryConfigurationExtension; /** - * {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableReactiveNeo4jRepositories} annotation. The - * {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's - * {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans - * during processing of configuration classes. + * {@link RepositoryBeanDefinitionRegistrarSupport} to enable + * {@link EnableReactiveNeo4jRepositories} annotation. The + * {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of + * Spring's {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, + * a dedicated SPI to register beans during processing of configuration classes. * * @author Gerrit Meier * @author Michael J. Simons @@ -32,21 +33,14 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi */ class ReactiveNeo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() - */ @Override protected Class getAnnotation() { return EnableReactiveNeo4jRepositories.class; } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() - */ @Override protected RepositoryConfigurationExtension getExtension() { return new ReactiveNeo4jRepositoryConfigurationExtension(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index 3c5147b58..e032e6f0a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; import org.apiguardian.api.API; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -35,10 +36,13 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; /** - * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide - * all necessary beans to be registered in the application's context before the user's "business" beans gets registered. + * This dedicated Neo4j repository extension will be registered via + * {@link Neo4jRepositoriesRegistrar} and then provide all necessary beans to be + * registered in the application's context before the user's "business" beans gets + * registered. *

- * While it is public, it is mainly used for internal API respectively for Spring Boots automatic configuration. + * While it is public, it is mainly used for internal API respectively for Spring Boots + * automatic configuration. * * @author Michael J. Simons * @author Gerrit Meier @@ -47,15 +51,21 @@ import org.springframework.data.repository.core.RepositoryMetadata; @API(status = API.Status.INTERNAL, since = "6.0") public final class ReactiveNeo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { - private static final String MODULE_PREFIX = "neo4j"; - /** * See {@link AbstractBeanDefinition#INFER_METHOD}. */ public static final String DEFAULT_NEO4J_CLIENT_BEAN_NAME = "reactiveNeo4jClient"; + /** + * The default name under which SDN expects a + * {@link org.springframework.data.neo4j.core.ReactiveNeo4jTemplate}. + */ public static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "reactiveNeo4jTemplate"; + /** + * The default name under which SDN expects a + * {@link org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager}. + */ public static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "reactiveTransactionManager"; /** @@ -68,10 +78,6 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit new StartupLogger(StartupLogger.Mode.REACTIVE).logStarting(); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() - */ @Override public String getRepositoryFactoryBeanClassName() { return ReactiveNeo4jRepositoryFactoryBean.class.getName(); @@ -94,19 +100,18 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit } @Override - public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { - // configurationSource.getSource() might be null and registerIfNotAlreadyRegistered is non-null api, + // configurationSource.getSource() might be null and + // registerIfNotAlreadyRegistered is non-null api, // but BeanMetadataAttributeAccessor will be eventually happy with a null value // noinspection ConstantConditions - registerIfNotAlreadyRegistered(() -> BeanDefinitionBuilder - .rootBeanDefinition(Neo4jEvaluationContextExtension.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .getBeanDefinition(), - registry, - Neo4jEvaluationContextExtension.class.getName(), - configurationSource.getSource() - ); + registerIfNotAlreadyRegistered( + () -> BeanDefinitionBuilder.rootBeanDefinition(Neo4jEvaluationContextExtension.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .getBeanDefinition(), + registry, Neo4jEvaluationContextExtension.class.getName(), configurationSource.getSource()); } @Override @@ -119,10 +124,6 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit return metadata.isReactiveRepository(); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) - */ @Override public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { @@ -133,4 +134,5 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit builder.addPropertyReference("mappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/StartupLogger.java b/src/main/java/org/springframework/data/neo4j/repository/config/StartupLogger.java index 1c1aa2187..fb412fe2e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/StartupLogger.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/StartupLogger.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.config; import java.util.Optional; import org.apache.commons.logging.LogFactory; + import org.springframework.core.log.LogAccessor; import org.springframework.data.neo4j.core.support.UserAgent; @@ -25,20 +26,9 @@ import org.springframework.data.neo4j.core.support.UserAgent; * Logs startup information. * * @author Michael J. Simons - * @soundtrack Helge & Hardcore - Jazz */ final class StartupLogger { - enum Mode { - IMPERATIVE("imperative"), REACTIVE("reactive"); - - final String displayValue; - - Mode(String displayValue) { - this.displayValue = displayValue; - } - } - private static final LogAccessor logger = new LogAccessor(LogFactory.getLog(StartupLogger.class)); private final Mode mode; @@ -60,16 +50,39 @@ final class StartupLogger { StringBuilder sb = new StringBuilder(); UserAgent userAgent = UserAgent.INSTANCE; - String sdnRx = Optional.ofNullable(userAgent.getSdnVersion()).map(v -> "SDN v" + v) - .orElse("an unknown version of SDN"); - String sdC = Optional.ofNullable(userAgent.getSpringDataVersion()).map(v -> "Spring Data Commons v" + v) - .orElse("an unknown version of Spring Data Commons"); - String driver = Optional.ofNullable(userAgent.getDriverVersion()).map(v -> "Neo4j Driver v" + v) - .orElse("an unknown version of the Neo4j Java Driver"); + String sdnRx = Optional.ofNullable(userAgent.getSdnVersion()) + .map(v -> "SDN v" + v) + .orElse("an unknown version of SDN"); + String sdC = Optional.ofNullable(userAgent.getSpringDataVersion()) + .map(v -> "Spring Data Commons v" + v) + .orElse("an unknown version of Spring Data Commons"); + String driver = Optional.ofNullable(userAgent.getDriverVersion()) + .map(v -> "Neo4j Driver v" + v) + .orElse("an unknown version of the Neo4j Java Driver"); - sb.append("Bootstrapping ").append(mode.displayValue).append(" Neo4j repositories based on ").append(sdnRx) - .append(" with ").append(sdC).append(" and ").append(driver).append("."); + sb.append("Bootstrapping ") + .append(this.mode.displayValue) + .append(" Neo4j repositories based on ") + .append(sdnRx) + .append(" with ") + .append(sdC) + .append(" and ") + .append(driver) + .append("."); return sb.toString(); } + + enum Mode { + + IMPERATIVE("imperative"), REACTIVE("reactive"); + + final String displayValue; + + Mode(String displayValue) { + this.displayValue = displayValue; + } + + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java index d5b5983c2..5197ec706 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java @@ -1,8 +1,22 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - Configuration infrastructure for Neo4j specific repositories, especially dedicated annotations to enable imperative - and reactive Spring Data Neo4j repositories. - * + * Configuration infrastructure for Neo4j specific repositories, + * especially dedicated annotations to enable imperative and reactive Spring Data Neo4j + * repositories. */ @NullMarked package org.springframework.data.neo4j.repository.config; diff --git a/src/main/java/org/springframework/data/neo4j/repository/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/package-info.java index e413b09f8..ad3c823a6 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/package-info.java @@ -1,7 +1,21 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - This package provides the Neo4j imperative and reactive repository API. - * + * This package provides the Neo4j imperative and reactive + * repository API. */ @NullMarked package org.springframework.data.neo4j.repository; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index f90207b7f..cb4455891 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -27,6 +27,7 @@ import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -61,11 +62,11 @@ import org.springframework.util.Assert; abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements RepositoryQuery { protected final Neo4jOperations neo4jOperations; + private final ProjectionFactory factory; AbstractNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - Neo4jQueryMethod queryMethod, - Neo4jQueryType queryType, ProjectionFactory factory) { + Neo4jQueryMethod queryMethod, Neo4jQueryType queryType, ProjectionFactory factory) { super(mappingContext, queryMethod, queryType); this.factory = factory; @@ -79,11 +80,8 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor return this.queryMethod; } - /** - * {@return whether the query is a geo near query} - */ boolean isGeoNearQuery() { - var repositoryMethod = queryMethod.getMethod(); + var repositoryMethod = this.queryMethod.getMethod(); Class returnType = repositoryMethod.getReturnType(); for (Class type : Neo4jQueryMethod.GEO_NEAR_RESULTS) { @@ -101,41 +99,48 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor } @Override - @Nullable - public final Object execute(Object[] parameters) { + @Nullable public final Object execute(Object[] parameters) { - boolean incrementLimit = queryMethod.incrementLimit(); + boolean incrementLimit = this.queryMethod.incrementLimit(); boolean geoNearQuery = isGeoNearQuery(); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), - parameters); + (Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), parameters); - ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + ResultProcessor resultProcessor = this.queryMethod.getResultProcessor() + .withDynamicProjection(parameterAccessor); ReturnedType returnedType = resultProcessor.getReturnedType(); PreparedQuery preparedQuery = prepareQuery(returnedType.getReturnedType(), - PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor, - null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity()); + PropertyFilterSupport.getInputProperties(resultProcessor, this.factory, this.mappingContext), + parameterAccessor, null, getMappingFunction(resultProcessor, geoNearQuery), + incrementLimit ? l -> l + 1 : UnaryOperator.identity()); - Object rawResult = new Neo4jQueryExecution.DefaultQueryExecution(neo4jOperations).execute(preparedQuery, queryMethod.asCollectionQuery()); + Object rawResult = new Neo4jQueryExecution.DefaultQueryExecution(this.neo4jOperations).execute(preparedQuery, + this.queryMethod.asCollectionQuery()); Converter preparingConverter = OptionalUnwrappingConverter.INSTANCE; if (returnedType.isProjecting()) { - DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext); + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + this.mappingContext); - // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case + // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the + // projecting case preparingConverter = source -> { var unwrapped = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source); - return (unwrapped == null) ? null : converter.convert(unwrapped); + return (unwrapped != null) ? converter.convert(unwrapped) : null; }; } - if (queryMethod.isPageQuery()) { + if (this.queryMethod.isPageQuery()) { rawResult = createPage(parameterAccessor, (List) rawResult); - } else if (queryMethod.isSliceQuery()) { + } + else if (this.queryMethod.isSliceQuery()) { rawResult = createSlice(incrementLimit, parameterAccessor, (List) rawResult); - } else if (queryMethod.isScrollQuery()) { - rawResult = createWindow(resultProcessor, incrementLimit, parameterAccessor, (List) rawResult, preparedQuery.getQueryFragmentsAndParameters()); - } else if (geoNearQuery) { + } + else if (this.queryMethod.isScrollQuery()) { + rawResult = createWindow(resultProcessor, incrementLimit, parameterAccessor, (List) rawResult, + preparedQuery.getQueryFragmentsAndParameters()); + } + else if (geoNearQuery) { rawResult = newGeoResults(rawResult); } @@ -146,11 +151,11 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor LongSupplier totalSupplier = () -> { - Supplier> defaultCountQuery = () -> prepareQuery(Long.class, - Collections.emptySet(), parameterAccessor, Neo4jQueryType.COUNT, null, UnaryOperator.identity()); + Supplier> defaultCountQuery = () -> prepareQuery(Long.class, Collections.emptySet(), + parameterAccessor, Neo4jQueryType.COUNT, null, UnaryOperator.identity()); PreparedQuery countQuery = getCountQuery(parameterAccessor).orElseGet(defaultCountQuery); - return neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult(); + return this.neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult(); }; if (isGeoNearQuery()) { @@ -165,21 +170,15 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor Pageable pageable = parameterAccessor.getPageable(); if (incrementLimit) { - return new SliceImpl<>( - rawResult.subList(0, Math.min(rawResult.size(), pageable.getPageSize())), + return new SliceImpl<>(rawResult.subList(0, Math.min(rawResult.size(), pageable.getPageSize())), PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()), - rawResult.size() > pageable.getPageSize() - ); - } else { - PreparedQuery countQuery = getCountQuery(parameterAccessor) - .orElseGet(() -> prepareQuery(Long.class, Collections.emptySet(), parameterAccessor, - Neo4jQueryType.COUNT, null, UnaryOperator.identity())); - long total = neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult(); - return new SliceImpl<>( - rawResult, - pageable, - pageable.getOffset() + pageable.getPageSize() < total - ); + rawResult.size() > pageable.getPageSize()); + } + else { + PreparedQuery countQuery = getCountQuery(parameterAccessor).orElseGet(() -> prepareQuery(Long.class, + Collections.emptySet(), parameterAccessor, Neo4jQueryType.COUNT, null, UnaryOperator.identity())); + long total = this.neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult(); + return new SliceImpl<>(rawResult, pageable, pageable.getOffset() + pageable.getPageSize() < total); } } @@ -192,4 +191,5 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor protected Optional> getCountQuery(Neo4jParameterAccessor parameterAccessor) { return Optional.empty(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java index 846c7d44c..5274e4a35 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java @@ -23,6 +23,8 @@ import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; +import reactor.core.publisher.Flux; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.GeoResult; import org.springframework.data.neo4j.core.PreparedQuery; @@ -40,8 +42,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; -import reactor.core.publisher.Flux; - /** * Base class for {@link RepositoryQuery} implementations for Neo4j. * @@ -52,10 +52,11 @@ import reactor.core.publisher.Flux; abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements RepositoryQuery { protected final ReactiveNeo4jOperations neo4jOperations; + private ProjectionFactory factory; AbstractReactiveNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - Neo4jQueryMethod queryMethod, Neo4jQueryType queryType, ProjectionFactory factory) { + Neo4jQueryMethod queryMethod, Neo4jQueryType queryType, ProjectionFactory factory) { super(mappingContext, queryMethod, queryType); @@ -69,11 +70,8 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R return this.queryMethod; } - /** - * {@return whether the query is a geo near query} - */ boolean isGeoNearQuery() { - var repositoryMethod = queryMethod.getMethod(); + var repositoryMethod = this.queryMethod.getMethod(); Class returnType = repositoryMethod.getReturnType(); for (Class type : Neo4jQueryMethod.GEO_NEAR_RESULTS) { @@ -92,43 +90,49 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R } @Override - @Nullable - public final Object execute(Object[] parameters) { + @Nullable public final Object execute(Object[] parameters) { - boolean incrementLimit = queryMethod.incrementLimit(); + boolean incrementLimit = this.queryMethod.incrementLimit(); boolean geoNearQuery = isGeoNearQuery(); - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), parameters); - ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), parameters); + ResultProcessor resultProcessor = this.queryMethod.getResultProcessor() + .withDynamicProjection(parameterAccessor); ReturnedType returnedType = resultProcessor.getReturnedType(); PreparedQuery preparedQuery = prepareQuery(returnedType.getReturnedType(), - PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor, - null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity()); + PropertyFilterSupport.getInputProperties(resultProcessor, this.factory, this.mappingContext), + parameterAccessor, null, getMappingFunction(resultProcessor, geoNearQuery), + incrementLimit ? l -> l + 1 : UnaryOperator.identity()); - Object rawResult = new Neo4jQueryExecution.ReactiveQueryExecution(neo4jOperations).execute(preparedQuery, - queryMethod.asCollectionQuery()); + Object rawResult = new Neo4jQueryExecution.ReactiveQueryExecution(this.neo4jOperations).execute(preparedQuery, + this.queryMethod.asCollectionQuery()); Converter preparingConverter = OptionalUnwrappingConverter.INSTANCE; if (returnedType.isProjecting()) { - DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext); + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + this.mappingContext); - // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case + // Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the + // projecting case preparingConverter = source -> { var intermediate = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source); - return (intermediate == null) ? null : converter.convert(intermediate); + return (intermediate != null) ? converter.convert(intermediate) : null; }; } - if (queryMethod.isScrollQuery()) { - rawResult = ((Flux) rawResult).collectList().map(rawResultList -> - createWindow(resultProcessor, incrementLimit, parameterAccessor, rawResultList, preparedQuery.getQueryFragmentsAndParameters())); + if (this.queryMethod.isScrollQuery()) { + rawResult = ((Flux) rawResult).collectList() + .map(rawResultList -> createWindow(resultProcessor, incrementLimit, parameterAccessor, rawResultList, + preparedQuery.getQueryFragmentsAndParameters())); } return resultProcessor.processResult(rawResult, preparingConverter); } protected abstract PreparedQuery prepareQuery(Class returnedType, - Collection includedProperties, Neo4jParameterAccessor parameterAccessor, - @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, - UnaryOperator limitModifier); + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, + UnaryOperator limitModifier); + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/BoundingBox.java b/src/main/java/org/springframework/data/neo4j/repository/query/BoundingBox.java index eee420c43..eceb0e265 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/BoundingBox.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/BoundingBox.java @@ -23,14 +23,23 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; /** - * This is a utility class that computes the bounding box of a polygon as a rectangle defined by the lower left and - * upper right point. + * This is a utility class that computes the bounding box of a polygon as a rectangle + * defined by the lower left and upper right point. * * @author Michael J. Simons * @since 6.0 */ public final class BoundingBox { + private final Point lowerLeft; + + private final Point upperRight; + + private BoundingBox(Point lowerLeft, Point upperRight) { + this.lowerLeft = lowerLeft; + this.upperRight = upperRight; + } + public static BoundingBox of(Polygon p) { return buildFrom(p.getPoints()); @@ -61,20 +70,12 @@ public final class BoundingBox { return new BoundingBox(new Point(minX, minY), new Point(maxX, maxY)); } - private final Point lowerLeft; - private final Point upperRight; - - private BoundingBox(Point lowerLeft, Point upperRight) { - this.lowerLeft = lowerLeft; - this.upperRight = upperRight; - } - public Point getLowerLeft() { - return lowerLeft; + return this.lowerLeft; } public Point getUpperRight() { - return upperRight; + return this.upperRight; } @Override @@ -86,16 +87,17 @@ public final class BoundingBox { return false; } BoundingBox that = (BoundingBox) o; - return lowerLeft.equals(that.lowerLeft) && upperRight.equals(that.upperRight); + return this.lowerLeft.equals(that.lowerLeft) && this.upperRight.equals(that.upperRight); } @Override public int hashCode() { - return Objects.hash(lowerLeft, upperRight); + return Objects.hash(this.lowerLeft, this.upperRight); } @Override public String toString() { - return "BoundingBox{" + "ll=" + lowerLeft + ", ur=" + upperRight + '}'; + return "BoundingBox{" + "ll=" + this.lowerLeft + ", ur=" + this.upperRight + '}'; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index c8dae9997..e0a8684cf 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.property; - import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; @@ -34,6 +32,7 @@ import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.SymbolicName; import org.neo4j.driver.Value; + import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition.Direction; import org.springframework.data.domain.Sort; @@ -43,6 +42,8 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.mapping.NodeDescription; +import static org.neo4j.cypherdsl.core.Cypher.property; + /** * Bridging between Spring Data domain Objects and Cypher constructs. * @@ -52,11 +53,14 @@ import org.springframework.data.neo4j.core.mapping.NodeDescription; @API(status = API.Status.INTERNAL, since = "6.0") public final class CypherAdapterUtils { + private CypherAdapterUtils() { + } + /** - * Maps Spring Data's {@link org.springframework.data.domain.Sort.Order} to a {@link SortItem}. See {@link #toSortItems(NodeDescription, Sort)}. - * + * Maps Spring Data's {@link org.springframework.data.domain.Sort.Order} to a + * {@link SortItem}. See {@link #toSortItems(NodeDescription, Sort)}. * @param nodeDescription {@link NodeDescription} to get properties for sorting from. - * @return A stream if sort items. Will be empty when sort is unsorted. + * @return a stream if sort items. Will be empty when sort is unsorted */ public static Function sortAdapterFor(NodeDescription nodeDescription) { return order -> { @@ -66,13 +70,16 @@ public final class CypherAdapterUtils { SymbolicName root; if (!propertyIsQualifiedOrComposite) { root = Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription); - } else { - // need to check first if this is really a qualified name or the "qualifier" is a composite property + } + else { + // need to check first if this is really a qualified name or the + // "qualifier" is a composite property if (nodeDescription.getGraphProperty(domainProperty.split("\\.")[0]).isEmpty()) { int indexOfSeparator = domainProperty.indexOf("."); root = Cypher.name(domainProperty.substring(0, indexOfSeparator)); domainProperty = domainProperty.substring(indexOfSeparator + 1); - } else { + } + else { root = Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription); } } @@ -84,22 +91,30 @@ public final class CypherAdapterUtils { optionalGraphProperty = nodeDescription.getGraphProperty(domainPropertyPrefix); } if (optionalGraphProperty.isEmpty()) { - throw new IllegalStateException(String.format("Cannot order by the unknown graph property: '%s'", domainProperty)); + throw new IllegalStateException( + String.format("Cannot order by the unknown graph property: '%s'", domainProperty)); } var graphProperty = optionalGraphProperty.get(); Expression expression; if (graphProperty.isInternalIdProperty()) { - // Not using the id expression here, as the root will be referring to the constructed map being returned. + // Not using the id expression here, as the root will be referring to the + // constructed map being returned. expression = property(root, Constants.NAME_OF_INTERNAL_ID); - } else if (graphProperty.isComposite() && !domainProperty.contains(".")) { - throw new IllegalStateException(String.format("Cannot order by composite property: '%s'. Only ordering by its nested fields is allowed.", domainProperty)); - } else if (graphProperty.isComposite()) { + } + else if (graphProperty.isComposite() && !domainProperty.contains(".")) { + throw new IllegalStateException(String.format( + "Cannot order by composite property: '%s'. Only ordering by its nested fields is allowed.", + domainProperty)); + } + else if (graphProperty.isComposite()) { if (nodeDescription.containsPossibleCircles(rpp -> true)) { expression = property(root, domainProperty); - } else { + } + else { expression = property(root, Constants.NAME_OF_ALL_PROPERTIES, domainProperty); } - } else { + } + else { expression = property(root, graphProperty.getPropertyName()); if (order.isIgnoreCase()) { expression = Cypher.toLower(expression); @@ -107,7 +122,8 @@ public final class CypherAdapterUtils { } SortItem sortItem = Cypher.sort(expression); - // Spring's Sort.Order defaults to ascending, so we just need to change this if we have descending order. + // Spring's Sort.Order defaults to ascending, so we just need to change this + // if we have descending order. if (order.isDescending()) { sortItem = sortItem.descending(); } @@ -115,7 +131,8 @@ public final class CypherAdapterUtils { }; } - public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity entity, KeysetScrollPosition scrollPosition, Sort sort, Neo4jConversionService conversionService) { + public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity entity, + KeysetScrollPosition scrollPosition, Sort sort, Neo4jConversionService conversionService) { var incomingKeys = scrollPosition.getKeys(); var orderedKeys = new LinkedHashMap(); @@ -142,7 +159,8 @@ public final class CypherAdapterUtils { var resultingCondition = Cypher.noCondition(); // This is the next equality pair if previous sort key was equal var nextEquals = Cypher.noCondition(); - // This is the condition for when all the sort orderedKeys are equal, and we must filter via id + // This is the condition for when all the sort orderedKeys are equal, and we must + // filter via id var allEqualsWithArtificialSort = Cypher.noCondition(); for (Map.Entry entry : orderedKeys.entrySet()) { @@ -150,7 +168,8 @@ public final class CypherAdapterUtils { var k = entry.getKey(); var v = entry.getValue(); if (v == null || (v instanceof Value value && value.isNull())) { - throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k)); + throw new IllegalStateException( + "Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k)); } var parameter = Cypher.anonParameter(conversionService.convert(v, Value.class)); @@ -159,14 +178,18 @@ public final class CypherAdapterUtils { var scrollDirection = scrollPosition.getDirection(); if (Constants.NAME_OF_ADDITIONAL_SORT.equals(k)) { expression = entity.getIdExpression(); - var comparatorFunction = getComparatorFunction(scrollPosition.scrollsForward() ? Sort.Direction.ASC : Sort.Direction.DESC, scrollDirection); - allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(comparatorFunction.apply(expression, parameter)); - } else if (propertyAndDirection.containsKey(k)) { + var comparatorFunction = getComparatorFunction( + scrollPosition.scrollsForward() ? Sort.Direction.ASC : Sort.Direction.DESC, scrollDirection); + allEqualsWithArtificialSort = allEqualsWithArtificialSort + .and(comparatorFunction.apply(expression, parameter)); + } + else if (propertyAndDirection.containsKey(k)) { var p = propertyAndDirection.get(k); expression = p.property.isIdProperty() ? entity.getIdExpression() : root.property(k); var comparatorFunction = getComparatorFunction(p.order.getDirection(), scrollDirection); - resultingCondition = resultingCondition.or(nextEquals.and(comparatorFunction.apply(expression, parameter))); + resultingCondition = resultingCondition + .or(nextEquals.and(comparatorFunction.apply(expression, parameter))); nextEquals = expression.eq(parameter); allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(nextEquals); } @@ -174,7 +197,8 @@ public final class CypherAdapterUtils { return resultingCondition.or(allEqualsWithArtificialSort); } - private static BiFunction getComparatorFunction(Sort.Direction sortDirection, KeysetScrollPosition.Direction scrollDirection) { + private static BiFunction getComparatorFunction(Sort.Direction sortDirection, + KeysetScrollPosition.Direction scrollDirection) { if (scrollDirection == Direction.BACKWARD) { return sortDirection.isAscending() ? Expression::lte : Expression::gte; } @@ -183,10 +207,9 @@ public final class CypherAdapterUtils { /** * Converts a Spring Data sort to an equivalent list of {@link SortItem sort items}. - * - * @param nodeDescription The node description to map the properties - * @param sort The sort object to convert - * @return An of sort items. It will be empty when sort is unsorted. + * @param nodeDescription the node description to map the properties + * @param sort the sort object to convert + * @return a collection of sort items which will be empty when sort is unsorted */ public static Collection toSortItems(@Nullable NodeDescription nodeDescription, Sort sort) { @@ -197,5 +220,4 @@ public final class CypherAdapterUtils { return sort.stream().map(sortAdapterFor(nodeDescription)).collect(Collectors.toList()); } - private CypherAdapterUtils() {} } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 7bace57b2..a1785bb76 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.point; - import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -42,6 +40,7 @@ import org.neo4j.cypherdsl.core.Property; import org.neo4j.cypherdsl.core.RelationshipPattern; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.driver.types.Point; + import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Pageable; @@ -66,9 +65,12 @@ import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +import static org.neo4j.cypherdsl.core.Cypher.point; + /** - * A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually creates Cypher queries as - * strings to be used by a Neo4j client or driver as statement template. + * A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually + * creates Cypher queries as strings to be used by a Neo4j client or driver as statement + * template. *

* This class is not thread safe and not reusable. * @@ -78,17 +80,21 @@ import org.springframework.data.repository.query.parser.PartTree; final class CypherQueryCreator extends AbstractQueryCreator { private final Neo4jMappingContext mappingContext; + private final NodeDescription nodeDescription; private final Neo4jQueryType queryType; + private final boolean isDistinct; private final Iterator formalParameters; + private final Queue lastParameter = new LinkedList<>(); private final Supplier indexSupplier = new IndexSupplier(); private final BiFunction, Object> parameterConversion; + private final List boundedParameters = new ArrayList<>(); private final Pageable pagingParameter; @@ -120,8 +126,9 @@ final class CypherQueryCreator extends AbstractQueryCreator limitModifier; - CypherQueryCreator(Neo4jMappingContext mappingContext, QueryMethod queryMethod, Class domainType, Neo4jQueryType queryType, PartTree tree, - Neo4jParameterAccessor actualParameters, Collection includedProperties, + CypherQueryCreator(Neo4jMappingContext mappingContext, QueryMethod queryMethod, Class domainType, + Neo4jQueryType queryType, PartTree tree, Neo4jParameterAccessor actualParameters, + Collection includedProperties, BiFunction, Object> parameterConversion, UnaryOperator limitModifier) { @@ -145,12 +152,14 @@ final class CypherQueryCreator extends AbstractQueryCreator new PropertyPathWrapper(symbolicNameIndex.getAndIncrement(), - mappingContext.getPersistentPropertyPath(part.getProperty()))) - .collect(Collectors.toList()); + this.propertyPathWrappers = tree.getParts() + .stream() + .map(part -> new PropertyPathWrapper(symbolicNameIndex.getAndIncrement(), + mappingContext.getPersistentPropertyPath(part.getProperty()))) + .collect(Collectors.toList()); - this.keysetRequiresSort = queryMethod.isScrollQuery() && actualParameters.getScrollPosition() instanceof KeysetScrollPosition; + this.keysetRequiresSort = queryMethod.isScrollQuery() + && actualParameters.getScrollPosition() instanceof KeysetScrollPosition; } @Override @@ -177,83 +186,99 @@ final class CypherQueryCreator extends AbstractQueryCreator convertedParameters = this.boundedParameters.stream() - .peek(p -> Neo4jQuerySupport.logParameterIfNull(p.nameOrIndex, p.value)) - .collect(Collectors.toMap(p -> p.nameOrIndex, p -> parameterConversion.apply(p.value, p.conversionOverride))); + .peek(p -> Neo4jQuerySupport.logParameterIfNull(p.nameOrIndex, p.value)) + .collect(Collectors.toMap(p -> p.nameOrIndex, + p -> this.parameterConversion.apply(p.value, p.conversionOverride))); QueryFragments queryFragments = createQueryFragments(condition, sort); - var theSort = pagingParameter.getSort().and(sort); - if (keysetRequiresSort && theSort.isUnsorted()) { + var theSort = this.pagingParameter.getSort().and(sort); + if (this.keysetRequiresSort && theSort.isUnsorted()) { throw new UnsupportedOperationException("Unsorted keyset based scrolling is not supported."); } - return new QueryFragmentsAndParameters(nodeDescription, queryFragments, convertedParameters, theSort); + return new QueryFragmentsAndParameters(this.nodeDescription, queryFragments, convertedParameters, theSort); } private QueryFragments createQueryFragments(@Nullable Condition condition, Sort sort) { QueryFragments queryFragments = new QueryFragments(); // all the ways we could query for - Node startNode = Cypher.node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels()) - .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); + Node startNode = Cypher.node(this.nodeDescription.getPrimaryLabel(), this.nodeDescription.getAdditionalLabels()) + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription)); Condition conditionFragment = Optional.ofNullable(condition).orElseGet(Cypher::noCondition); List relationshipChain = new ArrayList<>(); - for (PropertyPathWrapper possiblePathWithRelationship : propertyPathWrappers) { + for (PropertyPathWrapper possiblePathWithRelationship : this.propertyPathWrappers) { if (possiblePathWithRelationship.hasRelationships()) { - relationshipChain.add((RelationshipPattern) possiblePathWithRelationship.createRelationshipChain(startNode)); + relationshipChain + .add((RelationshipPattern) possiblePathWithRelationship.createRelationshipChain(startNode)); } } if (!relationshipChain.isEmpty()) { queryFragments.setMatchOn(relationshipChain); - } else { + } + else { queryFragments.addMatchOn(startNode); } // end of initial filter query creation - if (queryType == Neo4jQueryType.COUNT) { + if (this.queryType == Neo4jQueryType.COUNT) { queryFragments.setReturnExpression(Cypher.count(Cypher.asterisk()), true); - } else if (queryType == Neo4jQueryType.EXISTS) { - queryFragments.setReturnExpression(Cypher.count(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)).gt(Cypher.literalOf(0)), true); - } else if (queryType == Neo4jQueryType.DELETE) { - queryFragments.setDeleteExpression(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)); - queryFragments.setReturnExpression(Cypher.count(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)), true); - } else { + } + else if (this.queryType == Neo4jQueryType.EXISTS) { + queryFragments.setReturnExpression( + Cypher.count(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription)).gt(Cypher.literalOf(0)), + true); + } + else if (this.queryType == Neo4jQueryType.DELETE) { + queryFragments.setDeleteExpression(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription)); + queryFragments + .setReturnExpression(Cypher.count(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription)), true); + } + else { - var theSort = pagingParameter.getSort(); + var theSort = this.pagingParameter.getSort(); if (!Objects.equals(theSort, sort)) { theSort = theSort.and(sort); } - if (pagingParameter.isUnpaged() && scrollPosition == null && maxResults != null) { - queryFragments.setLimit(limitModifier.apply(maxResults.intValue())); - } else if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { + if (this.pagingParameter.isUnpaged() && this.scrollPosition == null && this.maxResults != null) { + queryFragments.setLimit(this.limitModifier.apply(this.maxResults.intValue())); + } + else if (this.scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { - Neo4jPersistentEntity entity = (Neo4jPersistentEntity) nodeDescription; - // Enforce sorting by something that is hopefully stable comparable (looking at Neo4j's id() with tears in my eyes). + Neo4jPersistentEntity entity = (Neo4jPersistentEntity) this.nodeDescription; + // Enforce sorting by something that is hopefully stable comparable + // (looking at Neo4j's id() with tears in my eyes). theSort = theSort.and(Sort.by(entity.getRequiredIdProperty().getName()).ascending()); - if (maxResults != null) { - queryFragments.setLimit(limitModifier.apply(maxResults.intValue())); + if (this.maxResults != null) { + queryFragments.setLimit(this.limitModifier.apply(this.maxResults.intValue())); } if (!keysetScrollPosition.isInitial()) { - conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort, mappingContext.getConversionService())); + conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, + keysetScrollPosition, theSort, this.mappingContext.getConversionService())); } queryFragments.setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); - } else if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { + } + else if (this.scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { if (!offsetScrollPosition.isInitial()) { queryFragments.setSkip(offsetScrollPosition.getOffset() + 1); } - queryFragments.setLimit(limitModifier.apply((pagingParameter.isUnpaged() && maxResults != null) ? maxResults.intValue() : pagingParameter.getPageSize())); + queryFragments + .setLimit(this.limitModifier.apply((this.pagingParameter.isUnpaged() && this.maxResults != null) + ? this.maxResults.intValue() : this.pagingParameter.getPageSize())); } var finalSortItems = new ArrayList<>(this.sortItems); - theSort.stream().map(CypherAdapterUtils.sortAdapterFor(nodeDescription)).forEach(finalSortItems::add); + theSort.stream().map(CypherAdapterUtils.sortAdapterFor(this.nodeDescription)).forEach(finalSortItems::add); - queryFragments.setReturnBasedOn(nodeDescription, includedProperties, isDistinct, this.distanceExpressions); + queryFragments.setReturnBasedOn(this.nodeDescription, this.includedProperties, this.isDistinct, + this.distanceExpressions); queryFragments.setOrderBy(finalSortItems); } @@ -265,15 +290,15 @@ final class CypherQueryCreator extends AbstractQueryCreator actualParameters) { - PersistentPropertyPath path = mappingContext.getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = this.mappingContext + .getPersistentPropertyPath(part.getProperty()); Neo4jPersistentProperty property = path.getLeafProperty(); boolean ignoreCase = ignoreCase(part); if (property.isComposite()) { - Condition compositePropertyCondition = CypherGenerator.INSTANCE.createCompositePropertyCondition( - property, + Condition compositePropertyCondition = CypherGenerator.INSTANCE.createCompositePropertyCondition(property, Cypher.name(getContainerName(path, (Neo4jPersistentEntity) property.getOwner())), toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); if (part.getType() == Part.Type.NEGATING_SIMPLE_PROPERTY) { @@ -284,40 +309,41 @@ final class CypherQueryCreator extends AbstractQueryCreator toCypherProperty(path, ignoreCase) - .gt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .gt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case BEFORE, LESS_THAN -> toCypherProperty(path, ignoreCase) - .lt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .lt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case BETWEEN -> betweenCondition(path, actualParameters, ignoreCase); - case CONTAINING -> containingCondition(path, property, actualParameters, ignoreCase); + case CONTAINING -> containingCondition(path, property, actualParameters, ignoreCase); case ENDING_WITH -> toCypherProperty(path, ignoreCase) - .endsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .endsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case EXISTS -> Cypher.exists(toCypherProperty(property)); case FALSE -> toCypherProperty(path, ignoreCase).isFalse(); case GREATER_THAN_EQUAL -> toCypherProperty(path, ignoreCase) - .gte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .gte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case IN -> toCypherProperty(path, ignoreCase) - .in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case IS_EMPTY -> toCypherProperty(path, ignoreCase).isEmpty(); case IS_NOT_EMPTY -> toCypherProperty(path, ignoreCase).isEmpty().not(); case IS_NOT_NULL -> toCypherProperty(path, ignoreCase).isNotNull(); case IS_NULL -> toCypherProperty(path, ignoreCase).isNull(); case LESS_THAN_EQUAL -> toCypherProperty(path, ignoreCase) - .lte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .lte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case LIKE -> likeCondition(path, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase); case NEAR -> createNearCondition(path, actualParameters); case NEGATING_SIMPLE_PROPERTY -> toCypherProperty(path, ignoreCase) - .isNotEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .isNotEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case NOT_CONTAINING -> containingCondition(path, property, actualParameters, ignoreCase).not(); case NOT_IN -> toCypherProperty(path, ignoreCase) - .in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)).not(); - case NOT_LIKE -> likeCondition(path, nextRequiredParameter(actualParameters, property).nameOrIndex, - ignoreCase).not(); + .in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)) + .not(); + case NOT_LIKE -> + likeCondition(path, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase).not(); case SIMPLE_PROPERTY -> toCypherProperty(path, ignoreCase) - .isEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .isEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case STARTING_WITH -> toCypherProperty(path, ignoreCase) - .startsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .startsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case REGEX -> toCypherProperty(path, ignoreCase) - .matches(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .matches(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); case TRUE -> toCypherProperty(path, ignoreCase).isTrue(); case WITHIN -> createWithinCondition(path, actualParameters); }; @@ -333,19 +359,19 @@ final class CypherQueryCreator extends AbstractQueryCreator owner = (Neo4jPersistentEntity) leafProperty.getOwner(); String containerName = getContainerName(path, owner); return toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase) - .in(Cypher.labels(Cypher.anyNode(containerName))); + .in(Cypher.labels(Cypher.anyNode(containerName))); } if (property.isCollectionLike()) { return toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase).in(cypherProperty); } return cypherProperty - .contains(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); + .contains(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)); } /** - * Checks whether to ignore the case for some operations. {@link PartTreeNeo4jQuery} will already have - * validated which properties can be made case insensitive given a certain keyword. - * + * Checks whether to ignore the case for some operations. {@link PartTreeNeo4jQuery} + * will already have validated which properties can be made case insensitive given a + * certain keyword. * @param part query part to get checked if case should get ignored * @return should the case get ignored */ @@ -361,12 +387,13 @@ final class CypherQueryCreator extends AbstractQueryCreator path, String parameterName, boolean ignoreCase) { String regexOptions = ignoreCase ? "(?i)" : ""; - return toCypherProperty(path, false).matches( - Cypher.literalOf(regexOptions + ".*").concat(Cypher.parameter(parameterName)).concat(Cypher.literalOf(".*"))); + return toCypherProperty(path, false).matches(Cypher.literalOf(regexOptions + ".*") + .concat(Cypher.parameter(parameterName)) + .concat(Cypher.literalOf(".*"))); } - private Condition betweenCondition(PersistentPropertyPath path, Iterator actualParameters, - boolean ignoreCase) { + private Condition betweenCondition(PersistentPropertyPath path, + Iterator actualParameters, boolean ignoreCase) { Neo4jPersistentProperty leafProperty = path.getLeafProperty(); Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters, leafProperty); @@ -374,14 +401,16 @@ final class CypherQueryCreator extends AbstractQueryCreator path, Iterator actualParameters) { + private Condition createNearCondition(PersistentPropertyPath path, + Iterator actualParameters) { Neo4jPersistentProperty leafProperty = path.getLeafProperty(); Parameter p1 = nextRequiredParameter(actualParameters, leafProperty); @@ -393,47 +422,58 @@ final class CypherQueryCreator extends AbstractQueryCreator owner = (Neo4jPersistentEntity) leafProperty.getOwner(); String containerName = getContainerName(path, owner); - this.distanceExpressions.add(distanceFunction.as("__distance_" + containerName + "_" + leafProperty.getPropertyName() + "__")); + this.distanceExpressions + .add(distanceFunction.as("__distance_" + containerName + "_" + leafProperty.getPropertyName() + "__")); this.sortItems.add(distanceFunction.ascending()); if (other.filter(p -> p.hasValueOfType(Distance.class)).isPresent()) { return distanceFunction.lte(toCypherParameter(other.get(), false)); - } else if (other.filter(p -> p.hasValueOfType(Range.class)).isPresent()) { + } + else if (other.filter(p -> p.hasValueOfType(Range.class)).isPresent()) { return createRangeConditionForExpression(distanceFunction, other.get()); - } else { - // We only have a point toCypherParameter, that's ok, but we have to put back the last toCypherParameter when it wasn't null + } + else { + // We only have a point toCypherParameter, that's ok, but we have to put back + // the last toCypherParameter when it wasn't null other.ifPresent(this.lastParameter::offer); - // A `NULL` distance makes no sense in a result asking for places nearby. It would be an arbitrary choice mapping null to zero or a max value. + // A `NULL` distance makes no sense in a result asking for places nearby. It + // would be an arbitrary choice mapping null to zero or a max value. return distanceFunction.isNotNull(); } } - private Condition createWithinCondition(PersistentPropertyPath path, Iterator actualParameters) { + private Condition createWithinCondition(PersistentPropertyPath path, + Iterator actualParameters) { Neo4jPersistentProperty leafProperty = path.getLeafProperty(); Parameter area = nextRequiredParameter(actualParameters, leafProperty); if (area.hasValueOfType(Circle.class)) { - // We don't know the CRS of the point, so we assume the same as the reference toCypherProperty - Expression referencePoint = point(Cypher.mapOf("x", createCypherParameter(area.nameOrIndex + ".x", false), "y", - createCypherParameter(area.nameOrIndex + ".y", false), "srid", + // We don't know the CRS of the point, so we assume the same as the reference + // toCypherProperty + Expression referencePoint = point(Cypher.mapOf("x", createCypherParameter(area.nameOrIndex + ".x", false), + "y", createCypherParameter(area.nameOrIndex + ".y", false), "srid", Cypher.property(toCypherProperty(path, false), "srid"))); Expression distanceFunction = Cypher.distance(toCypherProperty(path, false), referencePoint); return distanceFunction.lte(createCypherParameter(area.nameOrIndex + ".radius", false)); - } else if (area.hasValueOfType(BoundingBox.class) || area.hasValueOfType(Box.class)) { + } + else if (area.hasValueOfType(BoundingBox.class) || area.hasValueOfType(Box.class)) { Expression llx = createCypherParameter(area.nameOrIndex + ".llx", false); Expression lly = createCypherParameter(area.nameOrIndex + ".lly", false); Expression urx = createCypherParameter(area.nameOrIndex + ".urx", false); @@ -443,20 +483,23 @@ final class CypherQueryCreator extends AbstractQueryCreator path, boolean addToLower) { @@ -490,17 +534,26 @@ final class CypherQueryCreator extends AbstractQueryCreator path, Neo4jPersistentEntity owner) { + private String getContainerName(PersistentPropertyPath path, + Neo4jPersistentEntity owner) { if (owner.equals(this.nodeDescription) && path.getLength() == 1) { return Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription).getValue(); } - PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream() - .filter(rp -> rp.getPersistentPropertyPath().equals(path)).findFirst().get(); + PropertyPathWrapper propertyPathWrapper = this.propertyPathWrappers.stream() + .filter(rp -> rp.getPersistentPropertyPath().equals(path)) + .findFirst() + .get(); String cypherElementName; // this "entity" is a representation of a relationship with properties if (owner.isRelationshipPropertiesEntity()) { cypherElementName = propertyPathWrapper.getRelationshipName(); - } else { + } + else { cypherElementName = propertyPathWrapper.getNodeName(); } return cypherElementName; @@ -543,36 +600,40 @@ final class CypherQueryCreator extends AbstractQueryCreator nextOptionalParameter(Iterator actualParameters, Neo4jPersistentProperty property) { + private Optional nextOptionalParameter(Iterator actualParameters, + Neo4jPersistentProperty property) { - Parameter nextRequiredParameter = lastParameter.poll(); + Parameter nextRequiredParameter = this.lastParameter.poll(); if (nextRequiredParameter != null) { return Optional.of(nextRequiredParameter); - } else if (formalParameters.hasNext()) { - final Neo4jQueryMethod.Neo4jParameter parameter = formalParameters.next(); + } + else if (this.formalParameters.hasNext()) { + final Neo4jQueryMethod.Neo4jParameter parameter = this.formalParameters.next(); - Parameter boundedParameter = new Parameter(parameter.getName().orElseGet(indexSupplier), + Parameter boundedParameter = new Parameter(parameter.getName().orElseGet(this.indexSupplier), actualParameters.next(), property.getOptionalConverter()); - boundedParameters.add(boundedParameter); + this.boundedParameters.add(boundedParameter); return Optional.of(boundedParameter); - } else { + } + else { return Optional.empty(); } } private Parameter nextRequiredParameter(Iterator actualParameters, Neo4jPersistentProperty property) { - Parameter nextRequiredParameter = lastParameter.poll(); + Parameter nextRequiredParameter = this.lastParameter.poll(); if (nextRequiredParameter != null) { return nextRequiredParameter; - } else { - if (!formalParameters.hasNext()) { + } + else { + if (!this.formalParameters.hasNext()) { throw new IllegalStateException("Not enough formal, bindable parameters for parts"); } - final Neo4jQueryMethod.Neo4jParameter parameter = formalParameters.next(); - Parameter boundedParameter = new Parameter(parameter.getName().orElseGet(indexSupplier), + final Neo4jQueryMethod.Neo4jParameter parameter = this.formalParameters.next(); + Parameter boundedParameter = new Parameter(parameter.getName().orElseGet(this.indexSupplier), actualParameters.next(), property.getOptionalConverter()); - boundedParameters.add(boundedParameter); + this.boundedParameters.add(boundedParameter); return boundedParameter; } } @@ -593,18 +654,19 @@ final class CypherQueryCreator extends AbstractQueryCreator type) { - return type.isInstance(value); + return type.isInstance(this.value); } @Override public String toString() { - return "Parameter{" + "nameOrIndex='" + nameOrIndex + '\'' + ", value=" + value + '}'; + return "Parameter{" + "nameOrIndex='" + this.nameOrIndex + '\'' + ", value=" + this.value + '}'; } + } /** - * Provides unique, incrementing indexes for parameter. Parameter indexes in derived query methods are not necessary - * dense. + * Provides unique, incrementing indexes for parameter. Parameter indexes in derived + * query methods are not necessary dense. */ static final class IndexSupplier implements Supplier { @@ -612,7 +674,9 @@ final class CypherQueryCreator extends AbstractQueryCreator renderer) { - - return new CypherdslBasedQuery(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.DEFAULT, projectionFactory, renderer); - } - private final Function renderer; - private CypherdslBasedQuery(Neo4jOperations neo4jOperations, - Neo4jMappingContext mappingContext, + private CypherdslBasedQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, Neo4jQueryType queryType, ProjectionFactory projectionFactory, Function renderer) { super(neo4jOperations, mappingContext, queryMethod, queryType, projectionFactory); this.renderer = renderer; } + static CypherdslBasedQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, + Neo4jQueryMethod queryMethod, ProjectionFactory projectionFactory, Function renderer) { + + return new CypherdslBasedQuery(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.DEFAULT, + projectionFactory, renderer); + } + @Override protected PreparedQuery prepareQuery(Class returnedType, - Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, UnaryOperator limitModifier) { @@ -73,7 +74,7 @@ final class CypherdslBasedQuery extends AbstractNeo4jQuery { Assert.notEmpty(parameters, "Cypher based query methods must provide at least a statement parameter"); Statement statement; - if (queryMethod.isPageQuery()) { + if (this.queryMethod.isPageQuery()) { Assert.isInstanceOf(OngoingReadingAndReturn.class, parameters[0], "The first parameter to a Cypher based method must be an ongoing reading with a defined return clause"); Assert.isInstanceOf(Statement.class, parameters[1], @@ -82,21 +83,24 @@ final class CypherdslBasedQuery extends AbstractNeo4jQuery { "The third parameter to a Cypher based method must be a page request"); Pageable pageable = (Pageable) parameters[2]; statement = ((OngoingReadingAndReturn) parameters[0]) - .orderBy(CypherAdapterUtils.toSortItems(mappingContext.getNodeDescription(getDomainType(queryMethod)), pageable.getSort())) - .skip(pageable.getOffset()) - .limit(limitModifier.apply(pageable.getPageSize())) - .build(); - } else { - Assert.isInstanceOf(Statement.class, parameters[0], "The first parameter to a Cypher based method must be a statement"); + .orderBy(CypherAdapterUtils.toSortItems( + this.mappingContext.getNodeDescription(getDomainType(this.queryMethod)), pageable.getSort())) + .skip(pageable.getOffset()) + .limit(limitModifier.apply(pageable.getPageSize())) + .build(); + } + else { + Assert.isInstanceOf(Statement.class, parameters[0], + "The first parameter to a Cypher based method must be a statement"); statement = (Statement) parameters[0]; } Map boundParameters = statement.getCatalog().getParameters(); return PreparedQuery.queryFor(returnedType) - .withCypherQuery(renderer.apply(statement)) - .withParameters(boundParameters) - .usingMappingFunction(mappingFunction) - .build(); + .withCypherQuery(this.renderer.apply(statement)) + .withParameters(boundParameters) + .usingMappingFunction(mappingFunction) + .build(); } @Override @@ -105,7 +109,9 @@ final class CypherdslBasedQuery extends AbstractNeo4jQuery { // We verified this above Statement countStatement = (Statement) parameterAccessor.getValues()[1]; return Optional.of(PreparedQuery.queryFor(Long.class) - .withCypherQuery(renderer.apply(countStatement)) - .withParameters(countStatement.getCatalog().getParameters()).build()); + .withCypherQuery(this.renderer.apply(countStatement)) + .withParameters(countStatement.getCatalog().getParameters()) + .build()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java index c3bb934d3..1a2f4f3c3 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; - import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -29,6 +27,7 @@ import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -40,9 +39,13 @@ import org.springframework.data.neo4j.repository.support.CypherdslConditionExecu import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; import org.springframework.data.support.PageableExecutionUtils; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; + /** + * Imperative variant of the {@link CypherdslConditionExecutor}. + * + * @param the returned domain type * @author Michael J. Simons - * @param The returned domain type. * @since 6.1 */ @API(status = API.Status.INTERNAL, since = "6.1") @@ -65,61 +68,59 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi @Override public Optional findOne(Condition condition) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition) - ).getSingleResult(); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition)) + .getSingleResult(); } @Override public Collection findAll(Condition condition) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition) - ).getResults(); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition)) + .getResults(); } @Override public Collection findAll(Condition condition, Sort sort) { Predicate noFilter = PropertyFilter.NO_FILTER; - return this.neo4jOperations.toExecutableQuery( - metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSort( - this.metaData, condition, sort, null, noFilter - ) - ).getResults(); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSort(this.metaData, condition, sort, null, noFilter)) + .getResults(); } @Override public Collection findAll(Condition condition, SortItem... sortItems) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSortItems( - this.metaData, condition, Arrays.asList(sortItems) - ) - ).getResults(); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, + Arrays.asList(sortItems))) + .getResults(); } @Override public Collection findAll(SortItem... sortItems) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Cypher.noCondition(), Arrays.asList(sortItems)) - ).getResults(); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Cypher.noCondition(), + Arrays.asList(sortItems))) + .getResults(); } @Override public Page findAll(Condition condition, Pageable pageable) { Predicate noFilter = PropertyFilter.NO_FILTER; - List page = this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, condition, pageable, noFilter) - ).getResults(); + List page = this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, condition, pageable, noFilter)) + .getResults(); LongSupplier totalCountSupplier = () -> this.count(condition); return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); } @@ -128,7 +129,8 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi public long count(Condition condition) { Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, condition) - .returning(Cypher.count(asterisk())).build(); + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, statement.getCatalog().getParameters()); } @@ -136,4 +138,5 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi public boolean exists(Condition condition) { return count(condition) > 0; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java index d6ae36e26..40f2586f3 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java @@ -35,4 +35,5 @@ import org.apiguardian.api.API; @Query(exists = true) @API(status = API.Status.STABLE, since = "6.0") public @interface ExistsQuery { + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java index 219218ba9..f2a8b4115 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; + import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -39,13 +40,13 @@ import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuer import org.springframework.data.support.PageableExecutionUtils; /** - * Immutable implementation of a {@link FetchableFluentQuery}. All - * methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be + * Immutable implementation of a {@link FetchableFluentQuery}. All methods that return a + * {@link FetchableFluentQuery} return a new instance, the original instance won't be * modified. * + * @param the source type + * @param the result type if projected * @author Michael J. Simons - * @param Source type - * @param Result type * @since 6.2 */ @API(status = API.Status.INTERNAL, since = "6.2") @@ -61,29 +62,17 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im private final Function, Boolean> existsOperation; - FetchableFluentQueryByExample( - Example example, - Class resultType, - Neo4jMappingContext mappingContext, - FluentFindOperation findOperation, - Function, Long> countOperation, - Function, Boolean> existsOperation - ) { - this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), - null, null); + FetchableFluentQueryByExample(Example example, Class resultType, Neo4jMappingContext mappingContext, + FluentFindOperation findOperation, Function, Long> countOperation, + Function, Boolean> existsOperation) { + this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), null, + null); } - FetchableFluentQueryByExample( - Example example, - Class resultType, - Neo4jMappingContext mappingContext, - FluentFindOperation findOperation, - Function, Long> countOperation, - Function, Boolean> existsOperation, - Sort sort, - @Nullable Integer limit, - @Nullable Collection properties - ) { + FetchableFluentQueryByExample(Example example, Class resultType, Neo4jMappingContext mappingContext, + FluentFindOperation findOperation, Function, Long> countOperation, + Function, Boolean> existsOperation, Sort sort, @Nullable Integer limit, + @Nullable Collection properties) { super(resultType, sort, limit, properties); this.mappingContext = mappingContext; this.example = example; @@ -96,15 +85,16 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @SuppressWarnings("HiddenField") public FetchableFluentQuery sortBy(Sort sort) { - return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, + this.properties); } @Override @SuppressWarnings("HiddenField") public FetchableFluentQuery limit(int limit) { - return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, limit, this.properties); + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @@ -119,24 +109,23 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @SuppressWarnings("HiddenField") public FetchableFluentQuery project(Collection properties) { - return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort, this.limit, + mergeProperties(extractAllPaths(properties))); } @Override - @Nullable - public R oneValue() { + @Nullable public R oneValue() { - return findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, - createIncludedFieldsPredicate())) - .oneValue(); + return this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, this.example, this.sort, + this.limit, createIncludedFieldsPredicate())) + .oneValue(); } @Override - @Nullable - public R firstValue() { + @Nullable public R firstValue() { List all = all(); return all.isEmpty() ? null : all.get(0); @@ -145,21 +134,21 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @Override public List all() { - return findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, - createIncludedFieldsPredicate())) - .all(); + return this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, this.example, this.sort, + this.limit, createIncludedFieldsPredicate())) + .all(); } @Override public Page page(Pageable pageable) { - List page = findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, - createIncludedFieldsPredicate())) - .all(); + List page = this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithPageable(this.mappingContext, this.example, pageable, + createIncludedFieldsPredicate())) + .all(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); @@ -168,21 +157,23 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @Override public Window scroll(ScrollPosition scrollPosition) { Class domainType = this.example.getProbeType(); - Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entity = this.mappingContext.getRequiredPersistentEntity(domainType); - var skip = scrollPosition.isInitial() - ? 0 - : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + 1 - : 0; + var skip = scrollPosition.isInitial() ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) + ? offsetScrollPosition.getOffset() + 1 : 0; - Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getRequiredPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + Condition condition = (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) ? CypherAdapterUtils + .combineKeysetIntoCondition(this.mappingContext.getRequiredPersistentEntity(this.example.getProbeType()), + keysetScrollPosition, this.sort, this.mappingContext.getConversionService()) : null; - List rawResult = findOperation.find(domainType) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate())) - .all(); + List rawResult = this.findOperation.find(domainType) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(this.mappingContext, this.example, + condition, this.sort, (this.limit != null) ? this.limit + 1 : 1, skip, scrollPosition, + createIncludedFieldsPredicate())) + .all(); return scroll(scrollPosition, rawResult, entity); } @@ -194,11 +185,12 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im @Override public long count() { - return countOperation.apply(example); + return this.countOperation.apply(this.example); } @Override public boolean exists() { - return existsOperation.apply(example); + return this.existsOperation.apply(this.example); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java index c0b546d4a..87f54db5a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java @@ -21,9 +21,11 @@ import java.util.function.Function; import java.util.function.LongSupplier; import java.util.stream.Stream; +import com.querydsl.core.types.Predicate; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; + import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -36,18 +38,15 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import com.querydsl.core.types.Predicate; - /** - * Immutable implementation of a {@link FetchableFluentQuery}. All - * methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be + * Immutable implementation of a {@link FetchableFluentQuery}. All methods that return a + * {@link FetchableFluentQuery} return a new instance, the original instance won't be * modified. * + * @param the source type + * @param the result type if projected * @author Michael J. Simons - * @param Source type - * @param Result type * @since 6.2 - * @soundtrack Die Ärzte - Geräusch */ @API(status = API.Status.INTERNAL, since = "6.2") final class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { @@ -64,30 +63,17 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport private final Neo4jMappingContext mappingContext; - FetchableFluentQueryByPredicate( - Predicate predicate, - Neo4jMappingContext mappingContext, - Neo4jPersistentEntity metaData, - Class resultType, - FluentFindOperation findOperation, - Function countOperation, - Function existsOperation - ) { - this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null); + FetchableFluentQueryByPredicate(Predicate predicate, Neo4jMappingContext mappingContext, + Neo4jPersistentEntity metaData, Class resultType, FluentFindOperation findOperation, + Function countOperation, Function existsOperation) { + this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, + Sort.unsorted(), null, null); } - FetchableFluentQueryByPredicate( - Predicate predicate, - Neo4jMappingContext mappingContext, - Neo4jPersistentEntity metaData, - Class resultType, - FluentFindOperation findOperation, - Function countOperation, - Function existsOperation, - Sort sort, - @Nullable Integer limit, - @Nullable Collection properties - ) { + FetchableFluentQueryByPredicate(Predicate predicate, Neo4jMappingContext mappingContext, + Neo4jPersistentEntity metaData, Class resultType, FluentFindOperation findOperation, + Function countOperation, Function existsOperation, Sort sort, + @Nullable Integer limit, @Nullable Collection properties) { super(resultType, sort, limit, properties); this.predicate = predicate; this.mappingContext = mappingContext; @@ -101,51 +87,48 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @SuppressWarnings("HiddenField") public FetchableFluentQuery sortBy(Sort sort) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, + this.resultType, this.findOperation, this.countOperation, this.existsOperation, this.sort.and(sort), + this.limit, this.properties); } @Override @SuppressWarnings("HiddenField") public FetchableFluentQuery limit(int limit) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, limit, this.properties); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, + this.resultType, this.findOperation, this.countOperation, this.existsOperation, this.sort, limit, + this.properties); } @Override @SuppressWarnings("HiddenField") public FetchableFluentQuery as(Class resultType) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, - this.countOperation, this.existsOperation); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, + this.findOperation, this.countOperation, this.existsOperation); } @Override @SuppressWarnings("HiddenField") public FetchableFluentQuery project(Collection properties) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, + this.resultType, this.findOperation, this.countOperation, this.existsOperation, this.sort, this.limit, + mergeProperties(extractAllPaths(properties))); } @Override - @Nullable - public R oneValue() { + @Nullable public R oneValue() { - return findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndSort(metaData, - Cypher.adapt(predicate).asCondition(), - sort, - limit, - createIncludedFieldsPredicate())) - .oneValue(); + return this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndSort(this.metaData, + Cypher.adapt(this.predicate).asCondition(), this.sort, this.limit, createIncludedFieldsPredicate())) + .oneValue(); } @Override - @Nullable - public R firstValue() { + @Nullable public R firstValue() { List all = all(); return all.isEmpty() ? null : all.get(0); @@ -154,28 +137,21 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @Override public List all() { - return findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndSort(metaData, - Cypher.adapt(predicate).asCondition(), - sort, - limit, - createIncludedFieldsPredicate())) - .all(); + return this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndSort(this.metaData, + Cypher.adapt(this.predicate).asCondition(), this.sort, this.limit, createIncludedFieldsPredicate())) + .all(); } @Override public Page page(Pageable pageable) { - List page = findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndPageable(metaData, - Cypher.adapt(predicate).asCondition(), - pageable, - createIncludedFieldsPredicate())) - .all(); + List page = this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, + Cypher.adapt(this.predicate).asCondition(), pageable, createIncludedFieldsPredicate())) + .all(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); @@ -184,21 +160,21 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @Override public Window scroll(ScrollPosition scrollPosition) { - QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData, - Cypher.adapt(predicate).asCondition(), - (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService()) - : null), - scrollPosition, sort, - limit == null ? 1 : limit + 1, - createIncludedFieldsPredicate()); + QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters + .forConditionWithScrollPosition(this.metaData, Cypher.adapt(this.predicate).asCondition(), + ((scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) + ? CypherAdapterUtils.combineKeysetIntoCondition(this.metaData, keysetScrollPosition, + this.sort, this.mappingContext.getConversionService()) + : null), + scrollPosition, this.sort, (this.limit != null) ? this.limit + 1 : 1, + createIncludedFieldsPredicate()); - List rawResult = findOperation.find(metaData.getType()) - .as(resultType) - .matching(queryFragmentsAndParameters) - .all(); + List rawResult = this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(queryFragmentsAndParameters) + .all(); - return scroll(scrollPosition, rawResult, metaData); + return scroll(scrollPosition, rawResult, this.metaData); } @Override @@ -208,11 +184,12 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @Override public long count() { - return countOperation.apply(predicate); + return this.countOperation.apply(this.predicate); } @Override public boolean exists() { - return existsOperation.apply(predicate); + return this.existsOperation.apply(this.predicate); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java index babc59371..5dd018421 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java @@ -25,6 +25,7 @@ import java.util.function.IntFunction; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.ScrollPosition; @@ -35,11 +36,11 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.PropertyFilter; /** - * Supporting class containing some state and convenience methods for building fluent queries (both imperative and reactive). + * Supporting class containing some state and convenience methods for building fluent + * queries (both imperative and reactive). * + * @param the result type * @author Michael J. Simons - * @param The result type - * @soundtrack Die Ärzte - Geräusch */ abstract class FluentQuerySupport { @@ -52,22 +53,34 @@ abstract class FluentQuerySupport { protected final Set properties; - FluentQuerySupport( - Class resultType, - Sort sort, - @Nullable Integer limit, - @Nullable Collection properties - ) { + FluentQuerySupport(Class resultType, Sort sort, @Nullable Integer limit, + @Nullable Collection properties) { this.resultType = resultType; this.sort = sort; this.limit = limit; if (properties != null) { this.properties = new HashSet<>(properties); - } else { + } + else { this.properties = Set.of(); } } + private static boolean hasMoreElements(List result, @Nullable Integer limit) { + return !result.isEmpty() && result.size() > ((limit != null) ? limit : 0); + } + + private static List getSubList(List result, @Nullable Integer limit, + ScrollPosition.Direction scrollDirection) { + + if (limit != null && limit > 0 && result.size() > limit) { + return (scrollDirection != ScrollPosition.Direction.FORWARD) ? result.subList(1, limit + 1) + : result.subList(0, limit); + } + + return result; + } + final Predicate createIncludedFieldsPredicate() { if (this.properties == null || this.properties.isEmpty()) { @@ -87,25 +100,26 @@ abstract class FluentQuerySupport { final Window scroll(ScrollPosition scrollPosition, List rawResult, Neo4jPersistentEntity entity) { - var skip = scrollPosition.isInitial() - ? 0 - : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + 1 - : 0; + var skip = scrollPosition.isInitial() ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) + ? offsetScrollPosition.getOffset() + 1 : 0; - var scrollDirection = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ? keysetScrollPosition.getDirection() : ScrollPosition.Direction.FORWARD; + var scrollDirection = (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) + ? keysetScrollPosition.getDirection() : ScrollPosition.Direction.FORWARD; if (scrollDirection == ScrollPosition.Direction.BACKWARD) { Collections.reverse(rawResult); } - IntFunction positionFunction = null; + IntFunction positionFunction; if (scrollPosition instanceof OffsetScrollPosition) { positionFunction = OffsetScrollPosition.positionFunction(skip); - } else { + } + else { positionFunction = v -> { var accessor = entity.getPropertyAccessor(rawResult.get(v)); var keys = new LinkedHashMap(); - sort.forEach(o -> { + this.sort.forEach(o -> { // Storing the graph property name here var persistentProperty = entity.getRequiredPersistentProperty(o.getProperty()); keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty)); @@ -114,7 +128,8 @@ abstract class FluentQuerySupport { return ScrollPosition.forward(keys); }; } - return Window.from(getSubList(rawResult, limit, scrollDirection), positionFunction, hasMoreElements(rawResult, limit)); + return Window.from(getSubList(rawResult, this.limit, scrollDirection), positionFunction, + hasMoreElements(rawResult, this.limit)); } final Collection extractAllPaths(Collection projectingProperties) { @@ -132,16 +147,4 @@ abstract class FluentQuerySupport { return allPaths; } - private static boolean hasMoreElements(List result, @Nullable Integer limit) { - return !result.isEmpty() && result.size() > (limit != null ? limit : 0); - } - - private static List getSubList(List result, @Nullable Integer limit, ScrollPosition.Direction scrollDirection) { - - if (limit != null && limit > 0 && result.size() > limit) { - return scrollDirection == ScrollPosition.Direction.FORWARD ? result.subList(0, limit) : result.subList(1, limit + 1); - } - - return result; - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java index 308f8c400..91349c71e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java @@ -34,6 +34,7 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.convert.EntityWriter; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -52,22 +53,19 @@ import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.util.TypeInformation; /** - * A specialized version of an {@link EntityWriter} for Neo4j that traverses the entity and maps the entity, - * its association and other meta attributes into a couple of nested maps. The values in the map will either be - * other maps or Neo4j Driver {@link org.neo4j.driver.Value values}. + * A specialized version of an {@link EntityWriter} for Neo4j that traverses the entity + * and maps the entity, its association and other meta attributes into a couple of nested + * maps. The values in the map will either be other maps or Neo4j Driver + * {@link org.neo4j.driver.Value values}. * * @author Michael J. Simons - * @soundtrack Weekend - Scheiße Gechillt [feat. Koljah, Juse Ju] * @since 6.1.0 */ @API(status = API.Status.INTERNAL, since = "6.1.0") final class Neo4jNestedMapEntityWriter implements EntityWriter> { - static EntityWriter> forContext(Neo4jMappingContext context) { - return new Neo4jNestedMapEntityWriter(context); - } - private final Neo4jMappingContext mappingContext; + private final Neo4jConversionService conversionService; private Neo4jNestedMapEntityWriter(Neo4jMappingContext mappingContext) { @@ -76,6 +74,10 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter> forContext(Neo4jMappingContext context) { + return new Neo4jNestedMapEntityWriter(context); + } + @Override public void write(Object source, Map sink) { @@ -87,7 +89,8 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter writeImpl(@Nullable Object source, Map sink, Set seenObjects, boolean initialObject) { + Map writeImpl(@Nullable Object source, Map sink, Set seenObjects, + boolean initialObject) { if (source == null) { return sink; @@ -104,8 +107,8 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter { if (p.isAnnotationPresent(TargetNode.class)) { - Value target = Values.value(this.writeImpl(propertyAccessor.getProperty(p), new HashMap<>(), seenObjects, false)); + Value target = Values + .value(this.writeImpl(propertyAccessor.getProperty(p), new HashMap<>(), seenObjects, false)); sink.put("__target__", target); } }); @@ -143,7 +147,6 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter sink, Neo4jPersistentEntity entity, PersistentPropertyAccessor propertyAccessor, Set seenObjects) { @@ -155,43 +158,42 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter unifiedView = Optional - .ofNullable(context.getValue()) - .map(v -> v instanceof Collection col ? col : Collections.singletonList(v)) - .orElseGet(Collections::emptyList); + // Not using the Mapping support here so that we don't have to deal with the + // nested array lists. + Collection unifiedView = Optional.ofNullable(context.getValue()) + .map(v -> (v instanceof Collection col) ? col : Collections.singletonList(v)) + .orElseGet(Collections::emptyList); if (property.isDynamicAssociation()) { TypeInformation keyType = property.getTypeInformation().getRequiredComponentType(); - Map collect = unifiedView - .stream().filter(Objects::nonNull) - .flatMap(intoSingleMapEntries()) - .flatMap(intoSingleCollectionEntries()) - .map(relatedEntry -> { - String key = conversionService.writeValue(relatedEntry.getKey(), keyType, - property.getOptionalConverter()) - .asString(); + Map collect = unifiedView.stream() + .filter(Objects::nonNull) + .flatMap(intoSingleMapEntries()) + .flatMap(intoSingleCollectionEntries()) + .map(relatedEntry -> { + String key = this.conversionService + .writeValue(relatedEntry.getKey(), keyType, property.getOptionalConverter()) + .asString(); - Map relatedObjectProperties; - Object relatedObject = relatedEntry.getValue(); - relatedObjectProperties = extractPotentialRelationProperties(description, relatedObject, seenObjects); + Map relatedObjectProperties; + Object relatedObject = relatedEntry.getValue(); + relatedObjectProperties = extractPotentialRelationProperties(description, relatedObject, + seenObjects); - return new HashMap.SimpleEntry<>(key, relatedObjectProperties); - }) - .collect(Collectors.groupingBy( - Map.Entry::getKey, - Collectors.mapping(Map.Entry::getValue, Collectors.collectingAndThen(Collectors.toList(), Values::value))) - ); + return new HashMap.SimpleEntry<>(key, relatedObjectProperties); + }) + .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, + Collectors.collectingAndThen(Collectors.toList(), Values::value)))); if (!collect.isEmpty() && propertyMap != null) { propertyMap.putAll(collect); } - } else { - List relatedObjects = unifiedView - .stream().filter(Objects::nonNull) - .map(relatedObject -> extractPotentialRelationProperties(description, relatedObject, - seenObjects)) - .collect(Collectors.toList()); + } + else { + List relatedObjects = unifiedView.stream() + .filter(Objects::nonNull) + .map(relatedObject -> extractPotentialRelationProperties(description, relatedObject, seenObjects)) + .collect(Collectors.toList()); if (!relatedObjects.isEmpty() && propertyMap != null) { String type = description.getType(); @@ -209,7 +211,8 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter { if (e.getValue() instanceof Collection col) { return col.stream().map(v -> new AbstractMap.SimpleEntry<>(e.getKey(), v)); - } else { + } + else { return Stream.of(e); } }; @@ -219,13 +222,15 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter { if (e instanceof Map map) { return map.entrySet().stream(); - } else { + } + else { return Stream.of((Map.Entry) e); } }; } - private void addLabels(Map sink, Neo4jPersistentEntity entity, PersistentPropertyAccessor propertyAccessor) { + private void addLabels(Map sink, Neo4jPersistentEntity entity, + PersistentPropertyAccessor propertyAccessor) { if (entity.isRelationshipPropertiesEntity()) { return; @@ -233,21 +238,16 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter labels = new ArrayList<>(); labels.add(entity.getPrimaryLabel()); - entity.getDynamicLabelsProperty() - .map(p -> { - @SuppressWarnings("unchecked") - Collection propertyValue = (Collection) propertyAccessor.getProperty(p); - return propertyValue; - }) - .ifPresent(labels::addAll); + entity.getDynamicLabelsProperty().map(p -> { + @SuppressWarnings("unchecked") + Collection propertyValue = (Collection) propertyAccessor.getProperty(p); + return propertyValue; + }).ifPresent(labels::addAll); sink.put(Constants.NAME_OF_ALL_LABELS, Values.value(labels)); } - private Map extractPotentialRelationProperties( - RelationshipDescription description, - Object relatedObject, - Set seenObjects - ) { + private Map extractPotentialRelationProperties(RelationshipDescription description, + Object relatedObject, Set seenObjects) { if (!description.hasRelationshipProperties()) { return this.writeImpl(relatedObject, new HashMap<>(), seenObjects, false); @@ -255,12 +255,11 @@ final class Neo4jNestedMapEntityWriter implements EntityWriter relatedObjectProperties; - relatedObjectProperties = this - .writeImpl(tuple.getRelationshipProperties(), new HashMap<>(), - seenObjects, false); + relatedObjectProperties = this.writeImpl(tuple.getRelationshipProperties(), new HashMap<>(), seenObjects, + false); relatedObjectProperties.put("__target__", - this.writeImpl(tuple.getRelatedEntity(), new HashMap<>(), - seenObjects, false)); + this.writeImpl(tuple.getRelatedEntity(), new HashMap<>(), seenObjects, false)); return relatedObjectProperties; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jParameterAccessor.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jParameterAccessor.java index e7a0fb297..bab800501 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jParameterAccessor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jParameterAccessor.java @@ -21,13 +21,14 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; /** + * Support for creating query parameters. + * * @author Michael J. Simons */ final class Neo4jParameterAccessor extends ParametersParameterAccessor { /** * Creates a new {@link ParametersParameterAccessor}. - * * @param parameters must not be {@literal null}. * @param values must not be {@literal null}. */ @@ -45,4 +46,5 @@ final class Neo4jParameterAccessor extends ParametersParameterAccessor { public Object[] getValues() { return super.getValues(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java index ea02e53f1..74acd0861 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -22,9 +22,9 @@ import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; /** - * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a - * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractNeo4jQuery} can be executed in various - * flavors. + * Set of classes to contain query execution strategies. Depending (mostly) on the return + * type of a {@link org.springframework.data.repository.query.QueryMethod} a + * {@link AbstractNeo4jQuery} can be executed in various flavors. * * @author Michael J. Simons * @author Gerrit Meier @@ -46,13 +46,15 @@ interface Neo4jQueryExecution { @Override public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { - Neo4jOperations.ExecutableQuery executableQuery = neo4jOperations.toExecutableQuery(preparedQuery); + Neo4jOperations.ExecutableQuery executableQuery = this.neo4jOperations.toExecutableQuery(preparedQuery); if (asCollectionQuery) { return executableQuery.getResults(); - } else { + } + else { return executableQuery.getSingleResult(); } } + } class ReactiveQueryExecution implements Neo4jQueryExecution { @@ -66,14 +68,17 @@ interface Neo4jQueryExecution { @Override public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { - Mono> executableQuery = - neo4jOperations.toExecutableQuery(preparedQuery); + Mono> executableQuery = this.neo4jOperations + .toExecutableQuery(preparedQuery); if (asCollectionQuery) { return executableQuery.flatMapMany(q -> q.getResults()); - } else { + } + else { return executableQuery.flatMap(q -> q.getSingleResult()); } } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java index 14fd12403..20dc4a498 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; + import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -40,8 +41,11 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { private final Neo4jMappingContext mappingContext; + private final Neo4jOperations neo4jOperations; + private final ValueExpressionDelegate delegate; + private final Configuration configuration; public Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, @@ -52,9 +56,6 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { this.configuration = configuration; } - /* (non-Javadoc) - * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) - */ @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { @@ -63,15 +64,20 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { - return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod, + return StringBasedNeo4jQuery.create(this.neo4jOperations, this.mappingContext, this.delegate, queryMethod, namedQueries.getQuery(namedQueryName), factory); - } else if (queryMethod.hasQueryAnnotation()) { - return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod, + } + else if (queryMethod.hasQueryAnnotation()) { + return StringBasedNeo4jQuery.create(this.neo4jOperations, this.mappingContext, this.delegate, queryMethod, factory); - } else if (queryMethod.isCypherBasedProjection()) { - return CypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, factory, Renderer.getRenderer(configuration)::render); - } else { - return PartTreeNeo4jQuery.create(neo4jOperations, mappingContext, queryMethod, factory); + } + else if (queryMethod.isCypherBasedProjection()) { + return CypherdslBasedQuery.create(this.neo4jOperations, this.mappingContext, queryMethod, factory, + Renderer.getRenderer(this.configuration)::render); + } + else { + return PartTreeNeo4jQuery.create(this.neo4jOperations, this.mappingContext, queryMethod, factory); } } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java index f62507d4d..1427a7143 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -22,6 +22,7 @@ import java.util.Optional; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.geo.GeoPage; @@ -39,9 +40,10 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** - * Neo4j specific implementation of {@link QueryMethod}. It contains a custom implementation of {@link Parameter} which - * supports Neo4js specific placeholder as well as a convenient method to return either the parameters index or name - * without placeholder. + * Neo4j specific implementation of {@link QueryMethod}. It contains a custom + * implementation of {@link Parameter} which supports Neo4js specific placeholder as well + * as a convenient method to return either the parameters index or name without + * placeholder. * * @author Gerrit Meier * @author Michael J. Simons @@ -50,7 +52,8 @@ import org.springframework.util.StringUtils; */ class Neo4jQueryMethod extends QueryMethod { - static final List> GEO_NEAR_RESULTS = List.of(GeoResult.class, GeoResults.class, GeoPage.class); + static final List> GEO_NEAR_RESULTS = List.of(GeoResult.class, GeoResults.class, + GeoPage.class); /** * Optional query annotation of the method. @@ -65,9 +68,8 @@ class Neo4jQueryMethod extends QueryMethod { private final Method method; /** - * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following - * invocations of the method given. - * + * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the + * correct query to use for following invocations of the method given. * @param method must not be {@literal null}. * @param metadata must not be {@literal null}. * @param factory must not be {@literal null}. @@ -77,13 +79,12 @@ class Neo4jQueryMethod extends QueryMethod { } /** - * Allows configuring {@link #cypherBasedProjection} from inheriting classes. Not meant to be called outside the - * inheritance tree. - * + * Allows configuring {@link #cypherBasedProjection} from inheriting classes. Not + * meant to be called outside the inheritance tree. * @param method must not be {@literal null}. * @param metadata must not be {@literal null}. * @param factory must not be {@literal null}. - * @param cypherBasedProjection True if this points to a Cypher-DSL based projection. + * @param cypherBasedProjection true if this points to a Cypher-DSL based projection. */ Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, boolean cypherBasedProjection) { @@ -96,7 +97,7 @@ class Neo4jQueryMethod extends QueryMethod { } String getRepositoryName() { - return repositoryName; + return this.repositoryName; } boolean isCollectionLikeQuery() { @@ -104,23 +105,51 @@ class Neo4jQueryMethod extends QueryMethod { } boolean isCypherBasedProjection() { - return cypherBasedProjection; + return this.cypherBasedProjection; } /** - * @return True if the underlying method has been annotated with {@code @Query}. + * A flag if the query points to an annotated method that defines a query. + * @return true if the underlying method has been annotated with {@code @Query}. */ boolean hasQueryAnnotation() { return this.queryAnnotation != null; } /** - * @return the {@link Query} annotation that is applied to the method or an empty {@link Optional} if none available. + * If {@link #hasQueryAnnotation()} returns true, the query can be retrieved with this + * method. + * @return the {@link Query} annotation that is applied to the method or an empty + * {@link Optional} if none available. */ Optional getQueryAnnotation() { return Optional.ofNullable(this.queryAnnotation); } + @Override + public Class getReturnedObjectType() { + Class returnedObjectType = super.getReturnedObjectType(); + if (returnedObjectType.equals(GeoResult.class)) { + return getDomainClass(); + } + return returnedObjectType; + } + + boolean incrementLimit() { + return (this.isSliceQuery() + && this.getQueryAnnotation().map(Query::countQuery).filter(StringUtils::hasText).isEmpty()) + || this.isScrollQuery(); + } + + boolean asCollectionQuery() { + return this.isCollectionLikeQuery() || this.isPageQuery() || this.isSliceQuery() || this.isScrollQuery() + || GeoResults.class.isAssignableFrom(this.method.getReturnType()); + } + + Method getMethod() { + return this.method; + } + static class Neo4jParameters extends Parameters<@NonNull Neo4jParameters, @NonNull Neo4jParameter> { Neo4jParameters(ParametersSource parametersSource) { @@ -135,25 +164,18 @@ class Neo4jQueryMethod extends QueryMethod { protected Neo4jParameters createFrom(List parameters) { return new Neo4jParameters(parameters); } - } - @Override - public Class getReturnedObjectType() { - Class returnedObjectType = super.getReturnedObjectType(); - if (returnedObjectType.equals(GeoResult.class)) { - return getDomainClass(); - } - return returnedObjectType; } static class Neo4jParameter extends Parameter { private static final String NAMED_PARAMETER_TEMPLATE = "$%s"; + private static final String POSITION_PARAMETER_TEMPLATE = "$%d"; /** - * Creates a new {@link Parameter} for the given {@link MethodParameter} and {@link TypeInformation}. - * + * Creates a new {@link Parameter} for the given {@link MethodParameter} and + * {@link TypeInformation}. * @param parameter must not be {@literal null}. * @param domainType must not be {@literal null}. */ @@ -161,26 +183,17 @@ class Neo4jQueryMethod extends QueryMethod { super(parameter, domainType); } + @Override public String getPlaceholder() { if (isNamedParameter()) { return String.format(NAMED_PARAMETER_TEMPLATE, getName().orElseThrow()); - } else { + } + else { return String.format(POSITION_PARAMETER_TEMPLATE, getIndex()); } } + } - boolean incrementLimit() { - return (this.isSliceQuery() && this.getQueryAnnotation().map(Query::countQuery).filter(StringUtils::hasText).isEmpty()) || this.isScrollQuery(); - } - - boolean asCollectionQuery() { - return this.isCollectionLikeQuery() || this.isPageQuery() || this.isSliceQuery() || this.isScrollQuery() || - GeoResults.class.isAssignableFrom(this.method.getReturnType()); - } - - Method getMethod() { - return method; - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java index 7c4528d29..d9c173a72 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java @@ -38,6 +38,7 @@ import org.jspecify.annotations.Nullable; import org.neo4j.driver.Values; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.log.LogAccessor; import org.springframework.data.convert.EntityWriter; import org.springframework.data.domain.KeysetScrollPosition; @@ -66,8 +67,9 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** - * Some conversions used by both reactive and imperative Neo4j queries. While we try to separate reactive and imperative - * flows, it is cumbersome to repeat those conversions all over the place. + * Some conversions used by both reactive and imperative Neo4j queries. While we try to + * separate reactive and imperative flows, it is cumbersome to repeat those conversions + * all over the place. * * @author Gerrit Meier * @author Michael J. Simons @@ -76,29 +78,19 @@ import org.springframework.util.Assert; abstract class Neo4jQuerySupport { protected static final ValueExpressionParser SPEL_EXPRESSION_PARSER = ValueExpressionParser.create(); + static final LogAccessor REPOSITORY_QUERY_LOG = new LogAccessor(LogFactory.getLog(Neo4jQuerySupport.class)); + + private static final Set> VALID_RETURN_TYPES_FOR_DELETE = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Long.class, long.class, Void.class, void.class))); protected final Neo4jMappingContext mappingContext; + protected final Neo4jQueryMethod queryMethod; + /** * The query type. */ protected final Neo4jQueryType queryType; - private static final Set> VALID_RETURN_TYPES_FOR_DELETE = Collections.unmodifiableSet(new HashSet<>( - Arrays.asList(Long.class, long.class, Void.class, void.class))); - - static final LogAccessor REPOSITORY_QUERY_LOG = new LogAccessor(LogFactory.getLog(Neo4jQuerySupport.class)); - - /** - * Centralizes inquiry of the domain type to use the result processor of the query method as the point of truth. - * While this could be exposed on the query method itself, we would risk working with another type if at some point - * we osk the result processor only. - * - * @param queryMethod The query method whose domain type is requested - * @return The domain type of the given query method. - */ - static Class getDomainType(QueryMethod queryMethod) { - return queryMethod.getResultProcessor().getReturnedType().getDomainType(); - } Neo4jQuerySupport(Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, Neo4jQueryType queryType) { @@ -106,47 +98,37 @@ abstract class Neo4jQuerySupport { Assert.notNull(queryMethod, "Query method must not be null"); Assert.notNull(queryType, "Query type must not be null"); Assert.isTrue(queryType != Neo4jQueryType.DELETE || hasValidReturnTypeForDelete(queryMethod), - "A derived delete query can only return the number of deleted nodes as a long or void" - ); + "A derived delete query can only return the number of deleted nodes as a long or void"); this.mappingContext = mappingContext; this.queryMethod = queryMethod; this.queryType = queryType; } - protected final Supplier> getMappingFunction(final ResultProcessor resultProcessor, boolean isGeoNearQuery) { - - return () -> { - final ReturnedType returnedTypeMetadata = resultProcessor.getReturnedType(); - final Class returnedType = returnedTypeMetadata.getReturnedType(); - final Class domainType = returnedTypeMetadata.getDomainType(); - - final BiFunction mappingFunction; - - if (mappingContext.getConversionService().isSimpleType(returnedType)) { - // Clients automatically selects a single value mapping function. - // It will throw an error if the query contains more than one column. - mappingFunction = null; - } else if (returnedTypeMetadata.isProjecting()) { - mappingFunction = EntityInstanceWithSource.decorateMappingFunction( - this.mappingContext.getRequiredMappingFunctionFor(domainType)); - } else if (isGeoNearQuery) { - mappingFunction = decorateAsGeoResult(this.mappingContext.getRequiredMappingFunctionFor(domainType)); - } else { - mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType); - } - return mappingFunction; - }; + /** + * Centralizes inquiry of the domain type to use the result processor of the query + * method as the point of truth. While this could be exposed on the query method + * itself, we would risk working with another type if at some point we osk the result + * processor only. + * @param queryMethod the query method whose domain type is requested + * @return the domain type of the given query method. + */ + static Class getDomainType(QueryMethod queryMethod) { + return queryMethod.getResultProcessor().getReturnedType().getDomainType(); } - public static BiFunction decorateAsGeoResult(BiFunction target) { + static BiFunction decorateAsGeoResult(BiFunction target) { return (t, r) -> { Object intermediateResult = target.apply(t, r); - var distances = StreamSupport.stream(r.keys().spliterator(), false).filter(k -> k.startsWith("__distance_")).toList(); + var distances = StreamSupport.stream(r.keys().spliterator(), false) + .filter(k -> k.startsWith("__distance_")) + .toList(); if (distances.isEmpty()) { throw new RuntimeException("No distance has been returned by the query, cannot create `GeoResult`"); - } else if (distances.size() > 1) { - throw new RuntimeException("More than one distance has been returned by the query, cannot create `GeoResult`; avoid using multiple near operations when returning `GeoResult`"); + } + else if (distances.size() > 1) { + throw new RuntimeException( + "More than one distance has been returned by the query, cannot create `GeoResult`; avoid using multiple near operations when returning `GeoResult`"); } var distance = new Distance(r.get(distances.get(0)).asDouble() / 1000.0, Metrics.KILOMETERS); return new GeoResult<>(intermediateResult, distance); @@ -154,7 +136,8 @@ abstract class Neo4jQuerySupport { } private static boolean hasValidReturnTypeForDelete(Neo4jQueryMethod queryMethod) { - return VALID_RETURN_TYPES_FOR_DELETE.contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType()); + return VALID_RETURN_TYPES_FOR_DELETE + .contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType()); } static void logParameterIfNull(String name, @Nullable Object value) { @@ -164,172 +147,15 @@ abstract class Neo4jQuerySupport { } Supplier messageSupplier = () -> { - String pointer = name == null || name.trim().isEmpty() ? "An unknown parameter" : "$" + name; - return String.format("%s points to a literal `null` value during a comparison. " + - "The comparisons will always resolve to false and probably lead to an empty result.", + String pointer = (name == null || name.trim().isEmpty()) ? "An unknown parameter" : "$" + name; + return String.format( + "%s points to a literal `null` value during a comparison. " + + "The comparisons will always resolve to false and probably lead to an empty result.", pointer); }; REPOSITORY_QUERY_LOG.debug(messageSupplier); } - /** - * Converts parameter as needed by the query generated, which is not covered by standard conversion services. - * - * @param parameter The parameter to fit into the generated query. - * @return A parameter that fits the placeholders of a generated query - */ - final Object convertParameter(@Nullable Object parameter) { - return this.convertParameter(parameter, null); - } - - /** - * Converts parameter as needed by the query generated, which is not covered by standard conversion services. - * - * @param parameter The parameter to fit into the generated query. - * @param conversionOverride Passed to the entity converter if present. - * @return A parameter that fits the placeholders of a generated query - */ - final Object convertParameter(@Nullable Object parameter, @Nullable Neo4jPersistentPropertyConverter conversionOverride) { - - if (parameter == null) { - return Values.NULL; - } else if (parameter instanceof Range v) { - return convertRange(v); - } else if (parameter instanceof Distance v) { - return calculateDistanceInMeter(v); - } else if (parameter instanceof Circle v) { - return convertCircle(v); - } else if (parameter instanceof Instant v) { - return v.atOffset(ZoneOffset.UTC); - } else if (parameter instanceof Box v) { - return convertBox(v); - } else if (parameter instanceof BoundingBox v) { - return convertBoundingBox(v); - } - - if (parameter instanceof Collection col) { - Class type = TemplateSupport.findCommonElementType(col); - if (type != null && mappingContext.hasPersistentEntityFor(type)) { - - EntityWriter> objectMapEntityWriter = Neo4jNestedMapEntityWriter - .forContext(mappingContext); - - return col.stream().map(v -> { - Map result = new HashMap<>(); - objectMapEntityWriter.write(v, result); - return result; - }).collect(Collectors.toList()); - } - } - - if (mappingContext.hasPersistentEntityFor(parameter.getClass())) { - - Map result = new HashMap<>(); - Neo4jNestedMapEntityWriter.forContext(mappingContext).write(parameter, result); - return result; - } - - if (parameter instanceof Map mapValue) { - return mapValue.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, v -> convertParameter(v.getValue(), conversionOverride))); - } - - return mappingContext.getConversionService().writeValue(parameter, - TypeInformation.of(parameter.getClass()), conversionOverride); - } - - static class QueryContext { - - final String repositoryMethodName; - - final String template; - - final Map boundParameters; - - final String query; - - private boolean hasLiteralReplacementForSort = false; - - QueryContext(String repositoryMethodName, String template, Map boundParameters) { - this.repositoryMethodName = repositoryMethodName; - this.template = template; - this.boundParameters = boundParameters; - - String cypherQuery = this.template; - Comparator> byLengthDescending = Comparator.comparing(e -> e.getKey().length()); - byLengthDescending = byLengthDescending.reversed(); - List> entries = this.boundParameters.entrySet() - .stream().sorted(byLengthDescending) - .toList(); - for (var entry : entries) { - Object value = entry.getValue(); - if (!(value instanceof Neo4jSpelSupport.LiteralReplacement)) { - continue; - } - this.boundParameters.remove(entry.getKey()); - - String key = entry.getKey(); - cypherQuery = cypherQuery.replace("$" + key, ((Neo4jSpelSupport.LiteralReplacement) value).getValue()); - this.hasLiteralReplacementForSort = - this.hasLiteralReplacementForSort || - ((Neo4jSpelSupport.LiteralReplacement) value).getTarget() == Neo4jSpelSupport.LiteralReplacement.Target.SORT; - } - this.query = cypherQuery; - } - } - - void logWarningsIfNecessary(QueryContext queryContext, Neo4jParameterAccessor parameterAccessor) { - - // Log warning if necessary - if (!(queryContext.hasLiteralReplacementForSort || parameterAccessor.getSort().isUnsorted())) { - - Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> - String.format( - "You passed a sorted request to the custom query for '%s'. SDN won't apply any sort information from that object to the query. " - + "Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.", - queryContext.repositoryMethodName)); - - String fragment = CypherGenerator.INSTANCE.createOrderByFragment(parameterAccessor.getSort()); - if (fragment != null) { - Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> - String.format( - "One possible order clause matching your page request would be the following fragment:%n%s", - fragment)); - } - } - } - - final Window createWindow(ResultProcessor resultProcessor, boolean incrementLimit, Neo4jParameterAccessor parameterAccessor, List rawResult, QueryFragmentsAndParameters orderBy) { - - var domainType = resultProcessor.getReturnedType().getDomainType(); - var neo4jPersistentEntity = mappingContext.getRequiredPersistentEntity(domainType); - var limit = Objects.requireNonNull(orderBy.getQueryFragments().getLimit(), "Can't create a result window without a size (limit)").intValue() - (incrementLimit ? 1 : 0); - var scrollPosition = parameterAccessor.getScrollPosition(); - - var scrollDirection = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ? keysetScrollPosition.getDirection() : Direction.FORWARD; - if (scrollDirection == Direction.BACKWARD) { - Collections.reverse(rawResult); - } - - return Window.from(getSubList(rawResult, limit, scrollDirection), v -> { - if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { - return offsetScrollPosition.advanceBy(v); - } else { - var accessor = neo4jPersistentEntity.getPropertyAccessor(rawResult.get(v)); - var keys = new LinkedHashMap(); - orderBy.getSort().forEach(o -> { - // Storing the graph property name here - var persistentProperty = neo4jPersistentEntity.getRequiredPersistentProperty(o.getProperty()); - keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty)); - // keys.put(persistentProperty.getPropertyName(), conversionService.convert(accessor.getProperty(persistentProperty), Value.class)); - }); - keys.put(Constants.NAME_OF_ADDITIONAL_SORT, accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty())); - // keys.put(Constants.NAME_OF_ADDITIONAL_SORT, conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), Value.class)); - return ScrollPosition.forward(keys); - } - }, hasMoreElements(rawResult, limit)); - } - @SuppressWarnings("unchecked") static GeoResults newGeoResults(Object rawResult) { return new GeoResults<>((List>) rawResult, Metrics.KILOMETERS); @@ -342,12 +168,193 @@ abstract class Neo4jQuerySupport { private static List getSubList(List result, int limit, Direction scrollDirection) { if (limit > 0 && result.size() > limit) { - return scrollDirection == Direction.FORWARD ? result.subList(0, limit) : result.subList(1, limit + 1); + return (scrollDirection != Direction.FORWARD) ? result.subList(1, limit + 1) : result.subList(0, limit); } return result; } + private static double calculateDistanceInMeter(Distance distance) { + + if (distance.getMetric() == Metrics.KILOMETERS) { + double kilometersDivisor = 0.001d; + return distance.getValue() / kilometersDivisor; + + } + else if (distance.getMetric() == Metrics.MILES) { + double milesDivisor = 0.00062137d; + return distance.getValue() / milesDivisor; + + } + else { + return distance.getValue(); + } + } + + protected final Supplier> getMappingFunction( + final ResultProcessor resultProcessor, boolean isGeoNearQuery) { + + return () -> { + final ReturnedType returnedTypeMetadata = resultProcessor.getReturnedType(); + final Class returnedType = returnedTypeMetadata.getReturnedType(); + final Class domainType = returnedTypeMetadata.getDomainType(); + + final BiFunction mappingFunction; + + if (this.mappingContext.getConversionService().isSimpleType(returnedType)) { + // Clients automatically selects a single value mapping function. + // It will throw an error if the query contains more than one column. + mappingFunction = null; + } + else if (returnedTypeMetadata.isProjecting()) { + mappingFunction = EntityInstanceWithSource + .decorateMappingFunction(this.mappingContext.getRequiredMappingFunctionFor(domainType)); + } + else if (isGeoNearQuery) { + mappingFunction = decorateAsGeoResult(this.mappingContext.getRequiredMappingFunctionFor(domainType)); + } + else { + mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType); + } + return mappingFunction; + }; + } + + /** + * Converts parameter as needed by the query generated, which is not covered by + * standard conversion services. + * @param parameter the parameter to fit into the generated query. + * @return a parameter that fits the placeholders of a generated query + */ + final Object convertParameter(@Nullable Object parameter) { + return this.convertParameter(parameter, null); + } + + /** + * Converts parameter as needed by the query generated, which is not covered by + * standard conversion services. + * @param parameter the parameter to fit into the generated query. + * @param conversionOverride passed to the entity converter if present. + * @return a parameter that fits the placeholders of a generated query + */ + final Object convertParameter(@Nullable Object parameter, + @Nullable Neo4jPersistentPropertyConverter conversionOverride) { + + if (parameter == null) { + return Values.NULL; + } + else if (parameter instanceof Range v) { + return convertRange(v); + } + else if (parameter instanceof Distance v) { + return calculateDistanceInMeter(v); + } + else if (parameter instanceof Circle v) { + return convertCircle(v); + } + else if (parameter instanceof Instant v) { + return v.atOffset(ZoneOffset.UTC); + } + else if (parameter instanceof Box v) { + return convertBox(v); + } + else if (parameter instanceof BoundingBox v) { + return convertBoundingBox(v); + } + + if (parameter instanceof Collection col) { + Class type = TemplateSupport.findCommonElementType(col); + if (type != null && this.mappingContext.hasPersistentEntityFor(type)) { + + EntityWriter> objectMapEntityWriter = Neo4jNestedMapEntityWriter + .forContext(this.mappingContext); + + return col.stream().map(v -> { + Map result = new HashMap<>(); + objectMapEntityWriter.write(v, result); + return result; + }).collect(Collectors.toList()); + } + } + + if (this.mappingContext.hasPersistentEntityFor(parameter.getClass())) { + + Map result = new HashMap<>(); + Neo4jNestedMapEntityWriter.forContext(this.mappingContext).write(parameter, result); + return result; + } + + if (parameter instanceof Map mapValue) { + return mapValue.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, v -> convertParameter(v.getValue(), conversionOverride))); + } + + return this.mappingContext.getConversionService() + .writeValue(parameter, TypeInformation.of(parameter.getClass()), conversionOverride); + } + + void logWarningsIfNecessary(QueryContext queryContext, Neo4jParameterAccessor parameterAccessor) { + + // Log warning if necessary + if (!(queryContext.hasLiteralReplacementForSort || parameterAccessor.getSort().isUnsorted())) { + + Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> String.format( + "You passed a sorted request to the custom query for '%s'. SDN won't apply any sort information from that object to the query. " + + "Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.", + queryContext.repositoryMethodName)); + + String fragment = CypherGenerator.INSTANCE.createOrderByFragment(parameterAccessor.getSort()); + if (fragment != null) { + Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> String.format( + "One possible order clause matching your page request would be the following fragment:%n%s", + fragment)); + } + } + } + + final Window createWindow(ResultProcessor resultProcessor, boolean incrementLimit, + Neo4jParameterAccessor parameterAccessor, List rawResult, QueryFragmentsAndParameters orderBy) { + + var domainType = resultProcessor.getReturnedType().getDomainType(); + var neo4jPersistentEntity = this.mappingContext.getRequiredPersistentEntity(domainType); + var limit = Objects + .requireNonNull(orderBy.getQueryFragments().getLimit(), + "Can't create a result window without a size (limit)") + .intValue() - (incrementLimit ? 1 : 0); + var scrollPosition = parameterAccessor.getScrollPosition(); + + var scrollDirection = (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) + ? keysetScrollPosition.getDirection() : Direction.FORWARD; + if (scrollDirection == Direction.BACKWARD) { + Collections.reverse(rawResult); + } + + return Window.from(getSubList(rawResult, limit, scrollDirection), v -> { + if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { + return offsetScrollPosition.advanceBy(v); + } + else { + var accessor = neo4jPersistentEntity.getPropertyAccessor(rawResult.get(v)); + var keys = new LinkedHashMap(); + orderBy.getSort().forEach(o -> { + // Storing the graph property name here + var persistentProperty = neo4jPersistentEntity.getRequiredPersistentProperty(o.getProperty()); + keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty)); + // keys.put(persistentProperty.getPropertyName(), + // conversionService.convert(accessor.getProperty(persistentProperty), + // Value.class)); + }); + keys.put(Constants.NAME_OF_ADDITIONAL_SORT, + accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty())); + // keys.put(Constants.NAME_OF_ADDITIONAL_SORT, + // conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), + // Value.class)); + return ScrollPosition.forward(keys); + } + }, hasMoreElements(rawResult, limit)); + } + private Map convertRange(Range range) { Map map = new HashMap<>(); range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); @@ -381,18 +388,46 @@ abstract class Neo4jQuerySupport { return map; } - private static double calculateDistanceInMeter(Distance distance) { + static class QueryContext { - if (distance.getMetric() == Metrics.KILOMETERS) { - double kilometersDivisor = 0.001d; - return distance.getValue() / kilometersDivisor; + final String repositoryMethodName; - } else if (distance.getMetric() == Metrics.MILES) { - double milesDivisor = 0.00062137d; - return distance.getValue() / milesDivisor; + final String template; - } else { - return distance.getValue(); + final Map boundParameters; + + final String query; + + private boolean hasLiteralReplacementForSort = false; + + QueryContext(String repositoryMethodName, String template, Map boundParameters) { + this.repositoryMethodName = repositoryMethodName; + this.template = template; + this.boundParameters = boundParameters; + + String cypherQuery = this.template; + Comparator> byLengthDescending = Comparator.comparing(e -> e.getKey().length()); + byLengthDescending = byLengthDescending.reversed(); + List> entries = this.boundParameters.entrySet() + .stream() + .sorted(byLengthDescending) + .toList(); + for (var entry : entries) { + Object value = entry.getValue(); + if (!(value instanceof Neo4jSpelSupport.LiteralReplacement)) { + continue; + } + this.boundParameters.remove(entry.getKey()); + + String key = entry.getKey(); + cypherQuery = cypherQuery.replace("$" + key, ((Neo4jSpelSupport.LiteralReplacement) value).getValue()); + this.hasLiteralReplacementForSort = this.hasLiteralReplacementForSort + || ((Neo4jSpelSupport.LiteralReplacement) value) + .getTarget() == Neo4jSpelSupport.LiteralReplacement.Target.SORT; + } + this.query = cypherQuery; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryType.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryType.java index bcf0dbc69..28d9aa0f9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryType.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryType.java @@ -20,7 +20,7 @@ import java.util.function.Function; import org.springframework.data.repository.query.parser.PartTree; /** - * Describes the type of a query. The types are mutually exclusive. + * Describes the query type. All types are mutually exclusive. * * @author Michael J. Simons */ @@ -54,11 +54,11 @@ enum Neo4jQueryType { } /** - * Gets the corresponding query type or throws an exception if the definition is not unique. - * - * @param countQuery True if you want a query with count projection. - * @param existsQuery True if you want a query with exists projection. - * @param deleteQuery True if you want a delete query. + * Gets the corresponding query type or throws an exception if the definition is not + * unique. + * @param countQuery true if you want a query with count projection. + * @param existsQuery true if you want a query with exists projection. + * @param deleteQuery true if you want a delete query. * @return the query type * @throws IllegalArgumentException in case more than one parameter is true. */ @@ -89,4 +89,5 @@ enum Neo4jQueryType { return queryType; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java index e83e7f30d..5353ff961 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.support.schema_name.SchemaNames; + import org.springframework.core.env.StandardEnvironment; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -40,38 +41,73 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; /** - * This class provides a couple of extensions to the Spring Data Neo4j SpEL support. Its static functions are registered - * inside an {@link org.springframework.data.spel.spi.EvaluationContextExtension} that in turn will be provided as a root bean. + * This class provides a couple of extensions to the Spring Data Neo4j SpEL support. Its + * static functions are registered inside an + * {@link org.springframework.data.spel.spi.EvaluationContextExtension} that in turn will + * be provided as a root bean. * * @author Michael J. Simons - * @soundtrack Red Hot Chili Peppers - Californication * @since 6.0.2 */ @API(status = API.Status.INTERNAL, since = "6.0.2") public final class Neo4jSpelSupport { - public static String FUNCTION_LITERAL = "literal"; - public static String FUNCTION_ANY_OF = "anyOf"; - public static String FUNCTION_ALL_OF = "allOf"; - public static String FUNCTION_ORDER_BY = "orderBy"; + private static final String EXPRESSION_PARAMETER = "$1#{"; + + private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{"; + + private static final String ENTITY_NAME = "staticLabels"; + + private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME; + + private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}"; + + private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern + .compile("([:?])#\\{(?!" + ENTITY_NAME_VARIABLE + ")"); + + private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile("([:?])__HASH__\\{"); /** - * Takes {@code arg} and tries to either extract a {@link Sort sort} from it or cast it to a sort. That sort is - * than past to the {@link CypherGenerator} that renders a valid order by fragment which replaces the SpEL placeholder - * without further validation whether it's attributes are in the query or similar literal. - * - * @param arg The {@link Sort sort object} to order the result set of the final query. - * @return A literal replacement for a SpEL placeholder + * Constant under which literal functions are registered. + */ + public static String FUNCTION_LITERAL = "literal"; + + /** + * Constant for the {@code anyOf} expression. + */ + public static String FUNCTION_ANY_OF = "anyOf"; + + /** + * Constant for the {@code allOf} expression. + */ + public static String FUNCTION_ALL_OF = "allOf"; + + /** + * Constant for the {@code orderBy} expression. + */ + public static String FUNCTION_ORDER_BY = "orderBy"; + + private Neo4jSpelSupport() { + } + + /** + * Takes {@code arg} and tries to either extract a {@link Sort sort} from it or cast + * it to a sort. That sort is than past to the {@link CypherGenerator} that renders a + * valid order by fragment which replaces the SpEL placeholder without further + * validation whether it's attributes are in the query or similar literal. + * @param arg the {@link Sort sort object} to order the result set of the final query. + * @return a literal replacement for a SpEL placeholder */ - @Nullable public static LiteralReplacement orderBy(Object arg) { Sort sort = null; if (arg instanceof Pageable v) { sort = v.getSort(); - } else if (arg instanceof Sort v) { + } + else if (arg instanceof Sort v) { sort = v; - } else if (arg != null) { + } + else if (arg != null) { throw new IllegalArgumentException(arg.getClass() + " is not a valid order criteria"); } return StringBasedLiteralReplacement.withTargetAndValue(LiteralReplacement.Target.SORT, @@ -79,16 +115,16 @@ public final class Neo4jSpelSupport { } /** - * Turns the arguments of this function into a literal replacement for the SpEL placeholder (instead of creating - * Cypher parameters). - * - * @param arg The object that will be inserted as a literal String into the query. It's {@code toString()} method will be used. - * @return A literal replacement for a SpEL placeholder + * Turns the arguments of this function into a literal replacement for the SpEL + * placeholder (instead of creating Cypher parameters). + * @param arg the object that will be inserted as a literal String into the query. + * It's {@code toString()} method will be used. + * @return a literal replacement for a SpEL placeholder */ public static LiteralReplacement literal(Object arg) { - return StringBasedLiteralReplacement - .withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, arg == null ? "" : arg.toString()); + return StringBasedLiteralReplacement.withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, + (arg != null) ? arg.toString() : ""); } public static LiteralReplacement anyOf(Object arg) { @@ -100,17 +136,15 @@ public final class Neo4jSpelSupport { } private static LiteralReplacement labels(Object arg, String joinOn) { - return StringBasedLiteralReplacement - .withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, - arg == null ? "" : joinStrings(arg, joinOn) - ); + return StringBasedLiteralReplacement.withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, + (arg != null) ? joinStrings(arg, joinOn) : ""); } private static String joinStrings(Object arg, String joinOn) { if (arg instanceof Collection) { return ((Collection) arg).stream() - .map(o -> SchemaNames.sanitize(o.toString()).orElseThrow()) - .collect(Collectors.joining(joinOn)); + .map(o -> SchemaNames.sanitize(o.toString()).orElseThrow()) + .collect(Collectors.joining(joinOn)); } // we are so kind and also accept plain strings instead of collection @@ -118,121 +152,24 @@ public final class Neo4jSpelSupport { return (String) arg; } - throw new IllegalArgumentException( - String.format("Cannot process argument %s. Please note that only Collection and String are supported types.", arg)); + throw new IllegalArgumentException(String.format( + "Cannot process argument %s. Please note that only Collection and String are supported types.", + arg)); } - /** - * A marker interface that indicates a literal replacement in a query instead of a parameter replacement. This - * comes in handy in places where non-parameterizable things should be created dynamic, for example matching on - * set of dynamic labels, types order ordering in a dynamic way. - */ - public interface LiteralReplacement { - - /** - * The target of this replacement. While a replacement can be used theoretically everywhere in the query, the target - * can be used to infer a dedicated meaning of this replacement. - */ - enum Target { SORT, UNSPECIFIED } - - String getValue(); - - Target getTarget(); - } - - private static class StringBasedLiteralReplacement implements LiteralReplacement { - - /** - * Default number of cached instances. - */ - private static final int DEFAULT_CACHE_SIZE = 16; - - /** - * A small cache of instances of replacements. The cache key is the literal string value. Done to avoid - * the creation of too many small objects. - */ - private static final Map INSTANCES = - new LinkedHashMap<>(DEFAULT_CACHE_SIZE) { - @Serial - private static final long serialVersionUID = 195460174410223375L; - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > DEFAULT_CACHE_SIZE; - } - }; - - private static final StampedLock LOCK = new StampedLock(); - - static LiteralReplacement withTargetAndValue(LiteralReplacement.Target target, @Nullable String value) { - - String valueUsed = value == null ? "" : value; - String key = target.name() + "_" + valueUsed; - - long stamp = LOCK.tryOptimisticRead(); - if (LOCK.validate(stamp) && INSTANCES.containsKey(key)) { - return INSTANCES.get(key); - } - try { - stamp = LOCK.readLock(); - LiteralReplacement replacement = null; - while (replacement == null) { - if (INSTANCES.containsKey(key)) { - replacement = INSTANCES.get(key); - } else { - long writeStamp = LOCK.tryConvertToWriteLock(stamp); - if (LOCK.validate(writeStamp)) { - replacement = new StringBasedLiteralReplacement(target, valueUsed); - stamp = writeStamp; - INSTANCES.put(key, replacement); - } else { - LOCK.unlockRead(stamp); - stamp = LOCK.writeLock(); - } - } - } - return replacement; - } finally { - LOCK.unlock(stamp); - } - } - - private final Target target; - private final String value; - - private StringBasedLiteralReplacement(Target target, String value) { - this.target = target; - this.value = value; - } - - @Override - public String getValue() { - return value; - } - - @Override - public Target getTarget() { - return target; - } - } - - private static final String EXPRESSION_PARAMETER = "$1#{"; - private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{"; - - private static final String ENTITY_NAME = "staticLabels"; - private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME; - private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}"; - - private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile("([:?])#\\{(?!" + ENTITY_NAME_VARIABLE + ")"); - private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile("([:?])__HASH__\\{"); /** - * @param query the query expression potentially containing a SpEL expression. Must not be {@literal null}. - * @param metadata the {@link Neo4jPersistentEntity} for the given entity. Must not be {@literal null}. - * @param parser Must not be {@literal null}. - * @return A query in which some SpEL expression have been replaced with the result of evaluating the expression + * Renders a query that may contains SpEL expressions. + * @param query the query expression potentially containing a SpEL expression. Must + * not be {@literal null}. + * @param mappingContext the mapping context in which the query is rendered + * @param metadata the {@link Neo4jPersistentEntity} for the given entity. Must not be + * {@literal null}. + * @param parser must not be {@literal null}. + * @return a query in which some SpEL expression have been replaced with the result of + * evaluating the expression */ - public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMappingContext mappingContext, EntityMetadata metadata, - ValueExpressionParser parser) { + public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMappingContext mappingContext, + EntityMetadata metadata, ValueExpressionParser parser) { Assert.notNull(query, "query must not be null"); Assert.notNull(metadata, "metadata must not be null"); @@ -242,13 +179,16 @@ public final class Neo4jSpelSupport { return query; } - ValueEvaluationContext evalContext = ValueEvaluationContext.of(new StandardEnvironment(), new StandardEvaluationContext()); + ValueEvaluationContext evalContext = ValueEvaluationContext.of(new StandardEnvironment(), + new StandardEvaluationContext()); Neo4jPersistentEntity requiredPersistentEntity = mappingContext - .getRequiredPersistentEntity(metadata.getJavaType()); - evalContext.getEvaluationContext().setVariable(ENTITY_NAME, requiredPersistentEntity.getStaticLabels() - .stream() - .map(l -> SchemaNames.sanitize(l, true).orElseThrow()) - .collect(Collectors.joining(":"))); + .getRequiredPersistentEntity(metadata.getJavaType()); + evalContext.getEvaluationContext() + .setVariable(ENTITY_NAME, + requiredPersistentEntity.getStaticLabels() + .stream() + .map(l -> SchemaNames.sanitize(l, true).orElseThrow()) + .collect(Collectors.joining(":"))); query = potentiallyQuoteExpressionsParameter(query); @@ -271,8 +211,120 @@ public final class Neo4jSpelSupport { return EXPRESSION_PARAMETER_QUOTING.matcher(query).replaceAll(QUOTED_EXPRESSION_PARAMETER); } - private static boolean containsExpression(String query) { return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION); } + + /** + * A marker interface that indicates a literal replacement in a query instead of a + * parameter replacement. This comes in handy in places where non-parameterizable + * things should be created dynamic, for example matching on set of dynamic labels, + * types order ordering in a dynamic way. + */ + public interface LiteralReplacement { + + String getValue(); + + Target getTarget(); + + /** + * The target of this replacement. While a replacement can be used theoretically + * everywhere in the query, the target can be used to infer a dedicated meaning of + * this replacement. + */ + enum Target { + + /** + * Replaces the sort fragment. + */ + SORT, + /** + * Unspecified target. + */ + UNSPECIFIED + + } + + } + + private static final class StringBasedLiteralReplacement implements LiteralReplacement { + + /** + * Default number of cached instances. + */ + private static final int DEFAULT_CACHE_SIZE = 16; + + /** + * A small cache of instances of replacements. The cache key is the literal string + * value. Done to avoid the creation of too many small objects. + */ + private static final Map INSTANCES = new LinkedHashMap<>(DEFAULT_CACHE_SIZE) { + @Serial + private static final long serialVersionUID = 195460174410223375L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > DEFAULT_CACHE_SIZE; + } + }; + + private static final StampedLock LOCK = new StampedLock(); + + private final Target target; + + private final String value; + + private StringBasedLiteralReplacement(Target target, String value) { + this.target = target; + this.value = value; + } + + static LiteralReplacement withTargetAndValue(LiteralReplacement.Target target, @Nullable String value) { + + String valueUsed = (value != null) ? value : ""; + String key = target.name() + "_" + valueUsed; + + long stamp = LOCK.tryOptimisticRead(); + if (LOCK.validate(stamp) && INSTANCES.containsKey(key)) { + return INSTANCES.get(key); + } + try { + stamp = LOCK.readLock(); + LiteralReplacement replacement = null; + while (replacement == null) { + if (INSTANCES.containsKey(key)) { + replacement = INSTANCES.get(key); + } + else { + long writeStamp = LOCK.tryConvertToWriteLock(stamp); + if (LOCK.validate(writeStamp)) { + replacement = new StringBasedLiteralReplacement(target, valueUsed); + stamp = writeStamp; + INSTANCES.put(key, replacement); + } + else { + LOCK.unlockRead(stamp); + stamp = LOCK.writeLock(); + } + } + } + return replacement; + } + finally { + LOCK.unlock(stamp); + } + } + + @Override + public String getValue() { + return this.value; + } + + @Override + public Target getTarget() { + return this.target; + } + + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java b/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java index 5a5cec347..af5d6a5eb 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/OptionalUnwrappingConverter.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.query; import java.util.Optional; import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; /** @@ -27,14 +28,15 @@ import org.springframework.core.convert.converter.Converter; * @author Michael J. Simons */ enum OptionalUnwrappingConverter implements Converter { + INSTANCE; @Override - @Nullable - public Object convert(Object source) { + @Nullable public Object convert(Object source) { if (source instanceof Optional v) { return v.orElse(null); } return source; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index db05d4cd1..f50f45634 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -24,6 +24,7 @@ import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -44,12 +45,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private final PartTree tree; - public static RepositoryQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - Neo4jQueryMethod queryMethod, ProjectionFactory factory) { - return new PartTreeNeo4jQuery(neo4jOperations, mappingContext, queryMethod, - new PartTree(queryMethod.getName(), getDomainType(queryMethod)), factory); - } - private PartTreeNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, PartTree tree, ProjectionFactory factory) { super(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.fromPartTree(tree), factory); @@ -60,17 +55,29 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.tree.flatMap(OrPart::stream).forEach(validator::validatePart); } - @Override - protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable Supplier> mappingFunction, UnaryOperator limitModifier) { + static RepositoryQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, + Neo4jQueryMethod queryMethod, ProjectionFactory factory) { + return new PartTreeNeo4jQuery(neo4jOperations, mappingContext, queryMethod, + new PartTree(queryMethod.getName(), getDomainType(queryMethod)), factory); + } - CypherQueryCreator queryCreator = new CypherQueryCreator(mappingContext, queryMethod, getDomainType(queryMethod), - Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(tree)), tree, parameterAccessor, - includedProperties, this::convertParameter, limitModifier); + @Override + protected PreparedQuery prepareQuery(Class returnedType, + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, + @Nullable Supplier> mappingFunction, + UnaryOperator limitModifier) { + + CypherQueryCreator queryCreator = new CypherQueryCreator(this.mappingContext, this.queryMethod, + getDomainType(this.queryMethod), + Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(this.tree)), this.tree, + parameterAccessor, includedProperties, this::convertParameter, limitModifier); QueryFragmentsAndParameters queryAndParameters = queryCreator.createQuery(); - return PreparedQuery.queryFor(returnedType).withQueryFragmentsAndParameters(queryAndParameters) - .usingMappingFunction(mappingFunction).build(); + return PreparedQuery.queryFor(returnedType) + .withQueryFragmentsAndParameters(queryAndParameters) + .usingMappingFunction(mappingFunction) + .build(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java b/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java index 25c552303..7549aec03 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java @@ -32,6 +32,7 @@ import java.util.TreeSet; import java.util.stream.Collectors; import org.neo4j.driver.types.Point; + import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; @@ -40,34 +41,38 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** - * Support class for validating parts of either a {@link PartTreeNeo4jQuery} or the {@link ReactivePartTreeNeo4jQuery - * reactive pendant}. + * Support class for validating parts of either a {@link PartTreeNeo4jQuery} or the + * {@link ReactivePartTreeNeo4jQuery reactive pendant}. * * @author Michael J. Simons - * @soundtrack Antilopen Gang - Abwasser * @since 6.0 */ class PartValidator { /** - * A set of the temporal types that are directly passable to the driver and support a meaningful comparison in a - * temporal sense (after, before). See - * + * A set of the temporal types that are directly passable to the driver and support a + * meaningful comparison in a temporal sense (after, before). See */ private static final Set> COMPARABLE_TEMPORAL_TYPES; - static { - Set> hlp = new TreeSet<>(Comparator.comparing(Class::getName)); - hlp.addAll(Arrays.asList(LocalDate.class, OffsetTime.class, OffsetDateTime.class, LocalTime.class, ZonedDateTime.class, LocalDateTime.class, Instant.class)); - COMPARABLE_TEMPORAL_TYPES = Collections.unmodifiableSet(hlp); - } private static final EnumSet TYPES_SUPPORTING_CASE_INSENSITIVITY = EnumSet.of(Part.Type.CONTAINING, Part.Type.ENDING_WITH, Part.Type.LIKE, Part.Type.NEGATING_SIMPLE_PROPERTY, Part.Type.NOT_CONTAINING, Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH); - private static final EnumSet TYPES_SUPPORTED_FOR_COMPOSITES = EnumSet.of(Part.Type.SIMPLE_PROPERTY, Part.Type.NEGATING_SIMPLE_PROPERTY); + private static final EnumSet TYPES_SUPPORTED_FOR_COMPOSITES = EnumSet.of(Part.Type.SIMPLE_PROPERTY, + Part.Type.NEGATING_SIMPLE_PROPERTY); + + static { + Set> hlp = new TreeSet<>(Comparator.comparing(Class::getName)); + hlp.addAll(Arrays.asList(LocalDate.class, OffsetTime.class, OffsetDateTime.class, LocalTime.class, + ZonedDateTime.class, LocalDateTime.class, Instant.class)); + COMPARABLE_TEMPORAL_TYPES = Collections.unmodifiableSet(hlp); + } private final Neo4jMappingContext mappingContext; + private final Neo4jQueryMethod queryMethod; PartValidator(Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) { @@ -75,6 +80,20 @@ class PartValidator { this.queryMethod = queryMethod; } + private static String formatTypes(Collection types) { + return types.stream().flatMap(t -> t.getKeywords().stream()).collect(Collectors.joining(", ", "[", "]")); + } + + /** + * Checks whether the given part can be queried without case sensitivity. + * @param part query part to check if ignoring case sensitivity is possible + * @return true when {@code part} can be queried case-insensitive + */ + static boolean canIgnoreCase(Part part) { + return part.getProperty().getLeafType() == String.class + && TYPES_SUPPORTING_CASE_INSENSITIVITY.contains(part.getType()); + } + void validatePart(Part part) { validateIgnoreCase(part); @@ -91,54 +110,42 @@ class PartValidator { private void validateNotACompositeProperty(Part part) { - PersistentPropertyPath path = mappingContext - .getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = this.mappingContext + .getPersistentPropertyPath(part.getProperty()); Neo4jPersistentProperty property = path.getLeafProperty(); - Assert.isTrue(!property.isComposite(), "Can not derive query for '%s': Derived queries are not supported for composite properties"); + Assert.isTrue(!property.isComposite(), + "Can not derive query for '%s': Derived queries are not supported for composite properties"); } private void validateIgnoreCase(Part part) { Assert.isTrue(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), () -> String.format( "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", - queryMethod, formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); + this.queryMethod, formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); } private void validateTemporalProperty(Part part) { Assert.isTrue(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String.format( "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", - queryMethod, formatTypes(Collections.singletonList(part.getType())), COMPARABLE_TEMPORAL_TYPES)); + this.queryMethod, formatTypes(Collections.singletonList(part.getType())), COMPARABLE_TEMPORAL_TYPES)); } private void validateCollectionProperty(Part part) { Assert.isTrue(part.getProperty().getLeafProperty().isCollection(), - () -> String.format("Can not derive query for '%s': The keywords %s work only with collection properties", - queryMethod, formatTypes(Collections.singletonList(part.getType())))); + () -> String.format( + "Can not derive query for '%s': The keywords %s work only with collection properties", + this.queryMethod, formatTypes(Collections.singletonList(part.getType())))); } private void validatePointProperty(Part part) { Assert.isTrue( TypeInformation.of(Point.class) - .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), - () -> String.format("Can not derive query for '%s': %s works only with spatial properties", queryMethod, - part.getType())); + .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), + () -> String.format("Can not derive query for '%s': %s works only with spatial properties", + this.queryMethod, part.getType())); } - private static String formatTypes(Collection types) { - return types.stream().flatMap(t -> t.getKeywords().stream()).collect(Collectors.joining(", ", "[", "]")); - } - - /** - * Checks whether the given part can be queried without case sensitivity. - * - * @param part query part to check if ignoring case sensitivity is possible - * @return True when {@code part} can be queried case-insensitive. - */ - static boolean canIgnoreCase(Part part) { - return part.getProperty().getLeafType() == String.class - && TYPES_SUPPORTING_CASE_INSENSITIVITY.contains(part.getType()); - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java index f8f337373..b39da34aa 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.literalOf; -import static org.neo4j.cypherdsl.core.Cypher.parameter; -import static org.neo4j.cypherdsl.core.Cypher.property; - import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -35,6 +31,7 @@ import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.StatementBuilder; + import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.mapping.PropertyPath; @@ -50,17 +47,33 @@ import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import static org.neo4j.cypherdsl.core.Cypher.literalOf; +import static org.neo4j.cypherdsl.core.Cypher.parameter; +import static org.neo4j.cypherdsl.core.Cypher.property; + /** * Support class for "query by example" executors. *

- * This wraps all information necessary to predicate a match: A root condition and actual parameters to fill in formal - * parameters inside the condition. + * This wraps all information necessary to predicate a match: A root condition and actual + * parameters to fill in formal parameters inside the condition. * * @author Michael J. Simons * @since 6.0 */ final class Predicate { + private final Neo4jPersistentEntity neo4jPersistentEntity; + + private final Map parameters = new HashMap<>(); + + private final Set relationshipFields = new HashSet<>(); + + private Condition condition = Cypher.noCondition(); + + private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) { + this.neo4jPersistentEntity = neo4jPersistentEntity; + } + static Predicate create(Neo4jMappingContext mappingContext, Example example) { Neo4jPersistentEntity nodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType()); @@ -74,20 +87,26 @@ final class Predicate { Predicate predicate = new Predicate(nodeDescription); for (GraphPropertyDescription graphProperty : graphProperties) { - PropertyPath propertyPath = PropertyPath.from(graphProperty.getFieldName(), nodeDescription.getTypeInformation()); + PropertyPath propertyPath = PropertyPath.from(graphProperty.getFieldName(), + nodeDescription.getTypeInformation()); // create condition for every defined property - PropertyPathWrapper propertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(propertyPath), true); - addConditionAndParameters(mappingContext, nodeDescription, beanWrapper, mode, matcherAccessor, predicate, graphProperty, propertyPathWrapper); + PropertyPathWrapper propertyPathWrapper = new PropertyPathWrapper( + relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(propertyPath), + true); + addConditionAndParameters(mappingContext, nodeDescription, beanWrapper, mode, matcherAccessor, predicate, + graphProperty, propertyPathWrapper); } - processRelationships(mappingContext, example, nodeDescription, beanWrapper, mode, relationshipPatternCount, null, predicate); + processRelationships(mappingContext, example, nodeDescription, beanWrapper, mode, relationshipPatternCount, + null, predicate); return predicate; } - private static void processRelationships(Neo4jMappingContext mappingContext, Example example, @Nullable NodeDescription currentNodeDescription, - DirectFieldAccessFallbackBeanWrapper beanWrapper, ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount, - @Nullable PropertyPath propertyPath, Predicate predicate) { + private static void processRelationships(Neo4jMappingContext mappingContext, Example example, + @Nullable NodeDescription currentNodeDescription, DirectFieldAccessFallbackBeanWrapper beanWrapper, + ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount, @Nullable PropertyPath propertyPath, + Predicate predicate) { if (currentNodeDescription == null) { return; @@ -101,7 +120,8 @@ final class Predicate { continue; } - // Right now we are only accepting the first element of a collection as a filter entry. + // Right now we are only accepting the first element of a collection as a + // filter entry. // Maybe combining multiple entities with AND might make sense. if (relationshipObject instanceof Collection collection) { int collectionSize = collection.size(); @@ -114,34 +134,39 @@ final class Predicate { relationshipObject = collection.iterator().next(); } - NodeDescription relatedNodeDescription = mappingContext.getNodeDescription(relationshipObject.getClass()); + NodeDescription relatedNodeDescription = mappingContext + .getNodeDescription(relationshipObject.getClass()); // if we come from the root object, the path is probably _null_, // and it needs to get initialized with the property name of the relationship - PropertyPath nestedPropertyPath = propertyPath == null - ? PropertyPath.from(relationshipFieldName, currentNodeDescription.getUnderlyingClass()) - : propertyPath.nested(relationshipFieldName); + PropertyPath nestedPropertyPath = (propertyPath != null) ? propertyPath.nested(relationshipFieldName) + : PropertyPath.from(relationshipFieldName, currentNodeDescription.getUnderlyingClass()); - PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(nestedPropertyPath), false); + PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper( + relationshipPatternCount.incrementAndGet(), + mappingContext.getPersistentPropertyPath(nestedPropertyPath), false); predicate.addRelationship(nestedPropertyPathWrapper); if (relatedNodeDescription != null) { for (GraphPropertyDescription graphProperty : relatedNodeDescription.getGraphProperties()) { - addConditionAndParameters(mappingContext, (Neo4jPersistentEntity) relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, - new ExampleMatcherAccessor(example.getMatcher()), predicate, - graphProperty, nestedPropertyPathWrapper); + addConditionAndParameters(mappingContext, (Neo4jPersistentEntity) relatedNodeDescription, + new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, + new ExampleMatcherAccessor(example.getMatcher()), predicate, graphProperty, + nestedPropertyPathWrapper); } } - processRelationships(mappingContext, example, relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, relationshipPatternCount, + processRelationships(mappingContext, example, relatedNodeDescription, + new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, relationshipPatternCount, nestedPropertyPath, predicate); } } - private static void addConditionAndParameters(Neo4jMappingContext mappingContext, Neo4jPersistentEntity nodeDescription, DirectFieldAccessFallbackBeanWrapper beanWrapper, - ExampleMatcher.MatchMode mode, ExampleMatcherAccessor matcherAccessor, Predicate predicate, GraphPropertyDescription graphProperty, - PropertyPathWrapper wrapper) { + private static void addConditionAndParameters(Neo4jMappingContext mappingContext, + Neo4jPersistentEntity nodeDescription, DirectFieldAccessFallbackBeanWrapper beanWrapper, + ExampleMatcher.MatchMode mode, ExampleMatcherAccessor matcherAccessor, Predicate predicate, + GraphPropertyDescription graphProperty, PropertyPathWrapper wrapper) { String currentPath = graphProperty.getFieldName(); if (matcherAccessor.isIgnoredPath(currentPath)) { @@ -151,14 +176,14 @@ final class Predicate { boolean internalId = graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds(); String propertyName = graphProperty.getPropertyName(); - ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor - .getValueTransformerForPath(currentPath); + ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor.getValueTransformerForPath(currentPath); Optional optionalValue = transformer - .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); + .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); if (optionalValue.isEmpty()) { if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { - predicate.add(mode, property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName).isNull()); + predicate.add(mode, + property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName).isNull()); } return; } @@ -166,17 +191,25 @@ final class Predicate { Neo4jConversionService conversionService = mappingContext.getConversionService(); boolean isRootNode = predicate.neo4jPersistentEntity.equals(nodeDescription); - var theValue = optionalValue.map(v -> v instanceof Neo4jPropertyValueTransformers.NegatedValue negatedValue ? negatedValue.value() : v).get(); + var theValue = optionalValue.map( + v -> (v instanceof Neo4jPropertyValueTransformers.NegatedValue negatedValue) ? negatedValue.value() : v) + .get(); Condition condition; if (graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds()) { if (isRootNode) { condition = predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(theValue)); - } else { - condition = Objects.requireNonNull(nodeDescription.getIdDescription(), "No id description available, cannot compute a Cypher expression for retrieving or storing the id").asIdExpression(wrapper.getNodeName()).isEqualTo(literalOf(theValue)); } - } else { - Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName); + else { + condition = Objects.requireNonNull(nodeDescription.getIdDescription(), + "No id description available, cannot compute a Cypher expression for retrieving or storing the id") + .asIdExpression(wrapper.getNodeName()) + .isEqualTo(literalOf(theValue)); + } + } + else { + Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) + : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName); Expression parameter = parameter(wrapper.getNodeName() + propertyName); condition = property.isEqualTo(parameter); @@ -210,20 +243,8 @@ final class Predicate { return condition; } - private final Neo4jPersistentEntity neo4jPersistentEntity; - - private Condition condition = Cypher.noCondition(); - - private final Map parameters = new HashMap<>(); - - private final Set relationshipFields = new HashSet<>(); - - private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) { - this.neo4jPersistentEntity = neo4jPersistentEntity; - } - - public Condition getCondition() { - return condition; + Condition getCondition() { + return this.condition; } StatementBuilder.OrderableOngoingReadingAndWith useWithReadingFragment( @@ -243,15 +264,12 @@ final class Predicate { this.relationshipFields.add(propertyPathWrapper); } - public NodeDescription getNeo4jPersistentEntity() { - return neo4jPersistentEntity; + Map getParameters() { + return Collections.unmodifiableMap(this.parameters); } - public Map getParameters() { - return Collections.unmodifiableMap(parameters); + Set getPropertyPathWrappers() { + return this.relationshipFields; } - public Set getPropertyPathWrappers() { - return relationshipFields; - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java index 6c08cab67..341f675a9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java @@ -20,6 +20,7 @@ import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.ExposesRelationships; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.RelationshipPattern; + import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; @@ -28,11 +29,15 @@ import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.neo4j.core.schema.TargetNode; class PropertyPathWrapper { + private static final String NAME_OF_RELATED_FILTER_ENTITY = "m"; + private static final String NAME_OF_RELATED_FILTER_RELATIONSHIP = "r"; private final int index; + private final PersistentPropertyPath persistentPropertyPath; + private final int lengthModification; PropertyPathWrapper(int index, PersistentPropertyPath persistentPropertyPath) { @@ -45,29 +50,30 @@ class PropertyPathWrapper { this.lengthModification = hasPropertyEnding ? 0 : 1; } - public PersistentPropertyPath getPersistentPropertyPath() { - return persistentPropertyPath; + PersistentPropertyPath getPersistentPropertyPath() { + return this.persistentPropertyPath; } String getNodeName() { - return NAME_OF_RELATED_FILTER_ENTITY + "_" + index; + return NAME_OF_RELATED_FILTER_ENTITY + "_" + this.index; } String getRelationshipName() { - return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + index; + return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + this.index; } ExposesRelationships createRelationshipChain(ExposesRelationships existingRelationshipChain) { ExposesRelationships cypherRelationship = existingRelationshipChain; int cnt = 0; - for (PersistentProperty persistentProperty : persistentPropertyPath) { + for (PersistentProperty persistentProperty : this.persistentPropertyPath) { if (persistentProperty.isAssociation() && persistentProperty.isAnnotationPresent(TargetNode.class)) { break; } - RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation(); + RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty + .getAssociation(); if (relationshipDescription == null) { break; @@ -82,9 +88,11 @@ class PropertyPathWrapper { // length - 1 = last index // length - 2 = property on last node // length - 3 = last node itself - // length + 1 if there is no property ending but the path only goes until it reaches the relationship field - boolean lastNode = cnt > (persistentPropertyPath.getLength() - 3 + lengthModification); - boolean lastRelationship = cnt + 1 > (persistentPropertyPath.getLength() - 4 + lengthModification); + // length + 1 if there is no property ending but the path only goes until it + // reaches the relationship field + boolean lastNode = cnt > (this.persistentPropertyPath.getLength() - 3 + this.lengthModification); + boolean lastRelationship = cnt + + 1 > (this.persistentPropertyPath.getLength() - 4 + this.lengthModification); cnt = cnt + 1; // we don't yet if the condition will target a relationship property @@ -94,10 +102,8 @@ class PropertyPathWrapper { } cypherRelationship = switch (relationshipDescription.getDirection()) { - case OUTGOING -> cypherRelationship - .relationshipTo(relatedNode, relationshipDescription.getType()); - case INCOMING -> cypherRelationship - .relationshipFrom(relatedNode, relationshipDescription.getType()); + case OUTGOING -> cypherRelationship.relationshipTo(relatedNode, relationshipDescription.getType()); + case INCOMING -> cypherRelationship.relationshipFrom(relatedNode, relationshipDescription.getType()); }; if (lastNode || (isRelationshipPropertiesEntity && lastRelationship)) { @@ -109,14 +115,15 @@ class PropertyPathWrapper { } private boolean isRelationshipPropertiesEntity(@Nullable NodeDescription relationshipPropertiesEntity) { - return relationshipPropertiesEntity != null - && ((Neo4jPersistentEntity) relationshipPropertiesEntity) - .getPersistentProperty(TargetNode.class) != null; + return relationshipPropertiesEntity != null && ((Neo4jPersistentEntity) relationshipPropertiesEntity) + .getPersistentProperty(TargetNode.class) != null; } - // if there is no direct property access, the list size is greater than 1 and as a consequence has to contain + // if there is no direct property access, the list size is greater than 1 and as a + // consequence has to contain // relationships. boolean hasRelationships() { return this.persistentPropertyPath.getLength() > 1; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Query.java b/src/main/java/org/springframework/data/neo4j/repository/query/Query.java index b0bbe2c58..462a61865 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Query.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Query.java @@ -22,13 +22,15 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; + import org.springframework.data.annotation.QueryAnnotation; /** - * Annotation to provide Cypher statements that will be used for executing the method. The Cypher statement may contain - * named parameters as supported by the - * >Neo4j Java - * Driver. Those parameters will get bound to the arguments of the annotated method. + * Annotation to provide Cypher statements that will be used for executing the method. The + * Cypher statement may contain named parameters as supported by the >Neo4j + * Java Driver. Those parameters will get bound to the arguments of the annotated + * method. * * @author Michael J. Simons * @since 6.0 @@ -41,27 +43,35 @@ import org.springframework.data.annotation.QueryAnnotation; public @interface Query { /** - * The custom Cypher query to get executed and mapped back, if any return type is defined. + * The custom Cypher query to get executed and mapped back, if any return type is + * defined. + * @return a Cypher query */ String value() default ""; /** - * The Cypher statement for counting the total number of expected results. Only needed for methods returning pages or slices based on custom queries. + * The Cypher statement for counting the total number of expected results. Only needed + * for methods returning pages or slices based on custom queries. + * @return a Cypher query for counting entities */ String countQuery() default ""; /** + * A flag if the {@link #value()} should be used as counting query. * @return whether the query defined should be executed as count projection. */ boolean count() default false; /** + * A flag if the {@link #value()} should be used as existential query. * @return whether the query defined should be executed as exists projection. */ boolean exists() default false; /** + * A flag if the {@link #value()} should be used as deletion query. * @return whether the query defined should be used to delete nodes or relationships. */ boolean delete() default false; + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java index 5d4d1b43e..1279b2d11 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragments.java @@ -32,11 +32,12 @@ import org.neo4j.cypherdsl.core.PatternElement; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder; + import org.springframework.data.neo4j.core.mapping.CypherGenerator; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.mapping.NodeDescription; +import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.neo4j.core.schema.Property; /** @@ -47,53 +48,82 @@ import org.springframework.data.neo4j.core.schema.Property; */ @API(status = API.Status.INTERNAL, since = "6.0.4") public final class QueryFragments { + private List matchOn = new ArrayList<>(); + @Nullable private Condition condition; + private Collection returnExpressions = new ArrayList<>(); + @Nullable private Collection orderBy; + @Nullable private Number limit; + @Nullable private Long skip; + @Nullable private ReturnTuple returnTuple; + private boolean scalarValueReturn = false; + @Nullable private Expression deleteExpression; + /** - * This flag becomes {@literal true} for backward scrolling keyset pagination. Any {@code AbstractNeo4jQuery} will in turn reverse the result list. + * This flag becomes {@literal true} for backward scrolling keyset pagination. Any + * {@code AbstractNeo4jQuery} will in turn reverse the result list. */ private boolean requiresReverseSort = false; + @Nullable private Predicate projectingPropertyFilter; + // Yeah, would be kinda nice having a simple method in Cypher-DSL ;) + private static SortItem reverse(SortItem sortItem) { + + var sortedExpression = new AtomicReference(); + var sortDirection = new AtomicReference(); + + sortItem.accept(segment -> { + if (segment instanceof SortItem.Direction direction) { + sortDirection.compareAndSet(null, + (direction == SortItem.Direction.UNDEFINED || direction == SortItem.Direction.ASC) + ? SortItem.Direction.DESC : SortItem.Direction.ASC); + } + else if (segment instanceof Expression expression) { + sortedExpression.compareAndSet(null, expression); + } + }); + + // Default might not explicitly set. + sortDirection.compareAndSet(null, SortItem.Direction.DESC); + return Cypher.sort(sortedExpression.get(), sortDirection.get()); + } + public void addMatchOn(PatternElement match) { this.matchOn.add(match); } + public List getMatchOn() { + return this.matchOn; + } + public void setMatchOn(List match) { this.matchOn = match; } - public List getMatchOn() { - return matchOn; + @Nullable public Condition getCondition() { + return this.condition; } public void setCondition(@Nullable Condition condition) { this.condition = Optional.ofNullable(condition).orElse(Cypher.noCondition()); } - @Nullable - public Condition getCondition() { - return condition; - } - - public void setReturnExpressions(Collection expression) { - this.returnExpressions = expression; - } - public void setDeleteExpression(@Nullable Expression expression) { this.deleteExpression = expression; } @@ -102,39 +132,30 @@ public final class QueryFragments { if (returnExpression != null) { this.returnExpressions = Collections.singletonList(returnExpression); this.scalarValueReturn = isScalarValue; - } else { + } + else { this.returnExpressions = List.of(); } } - public void setProjectingPropertyFilter(@Nullable Predicate projectingPropertyFilter) { + public void setProjectingPropertyFilter( + @Nullable Predicate projectingPropertyFilter) { this.projectingPropertyFilter = projectingPropertyFilter; } public boolean includeField(PropertyFilter.RelaxedPropertyPath fieldName) { - return (projectingPropertyFilter == null || projectingPropertyFilter.test(fieldName)) + return (this.projectingPropertyFilter == null || this.projectingPropertyFilter.test(fieldName)) && (this.returnTuple == null || this.returnTuple.include(fieldName)); } - public void setOrderBy(@Nullable Collection orderBy) { - this.orderBy = orderBy; - } - - public void setLimit(Number limit) { - this.limit = limit; - } - - public void setSkip(Long skip) { - this.skip = skip; - } - - public void setReturnBasedOn(NodeDescription nodeDescription, Collection includedProperties, - boolean isDistinct, List additionalExpressions) { + public void setReturnBasedOn(NodeDescription nodeDescription, + Collection includedProperties, boolean isDistinct, + List additionalExpressions) { this.returnTuple = new ReturnTuple(nodeDescription, includedProperties, isDistinct, additionalExpressions); } public boolean isScalarValueReturn() { - return scalarValueReturn; + return this.scalarValueReturn; } public void setRequiresReverseSort(boolean requiresReverseSort) { @@ -147,95 +168,97 @@ public final class QueryFragments { throw new IllegalStateException("No pattern to match on"); } - StatementBuilder.OngoingReadingWithoutWhere match = Cypher.match(matchOn.get(0)); + StatementBuilder.OngoingReadingWithoutWhere match = Cypher.match(this.matchOn.get(0)); - for (PatternElement patternElement : matchOn) { + for (PatternElement patternElement : this.matchOn) { match = match.match(patternElement); } - StatementBuilder.OngoingReadingWithWhere matchWithWhere = match.where(condition); + StatementBuilder.OngoingReadingWithWhere matchWithWhere = match.where(this.condition); - if (deleteExpression != null) { - matchWithWhere = (StatementBuilder.OngoingReadingWithWhere) matchWithWhere.detachDelete(deleteExpression); + if (this.deleteExpression != null) { + matchWithWhere = (StatementBuilder.OngoingReadingWithWhere) matchWithWhere + .detachDelete(this.deleteExpression); } StatementBuilder.OngoingReadingAndReturn returnPart = isDistinctReturn() ? matchWithWhere.returningDistinct(getReturnExpressions()) : matchWithWhere.returning(getReturnExpressions()); - Statement statement = returnPart - .orderBy(getOrderBy()) - .skip(skip) - .limit(limit).build(); + Statement statement = returnPart.orderBy(getOrderBy()).skip(this.skip).limit(this.limit).build(); statement.setRenderConstantsAsParameters(false); return statement; } private Collection getReturnExpressions() { - return returnExpressions.isEmpty() && returnTuple != null ? CypherGenerator.INSTANCE.createReturnStatementForMatch((Neo4jPersistentEntity) returnTuple.nodeDescription, - this::includeField, returnTuple.additionalExpressions.toArray(Expression[]::new)) : returnExpressions; + return (this.returnExpressions.isEmpty() && this.returnTuple != null) ? CypherGenerator.INSTANCE + .createReturnStatementForMatch((Neo4jPersistentEntity) this.returnTuple.nodeDescription, + this::includeField, this.returnTuple.additionalExpressions.toArray(Expression[]::new)) + : this.returnExpressions; + } + + public void setReturnExpressions(Collection expression) { + this.returnExpressions = expression; } public Collection getAdditionalReturnExpressions() { - return this.returnTuple == null ? List.of() : returnTuple.additionalExpressions; + return (this.returnTuple != null) ? this.returnTuple.additionalExpressions : List.of(); } private boolean isDistinctReturn() { - return returnExpressions.isEmpty() && returnTuple != null && returnTuple.isDistinct; + return this.returnExpressions.isEmpty() && this.returnTuple != null && this.returnTuple.isDistinct; } public Collection getOrderBy() { - if (orderBy == null) { + if (this.orderBy == null) { return List.of(); - } else if (!requiresReverseSort) { - return orderBy; - } else { - return orderBy.stream().map(QueryFragments::reverse).toList(); + } + else if (!this.requiresReverseSort) { + return this.orderBy; + } + else { + return this.orderBy.stream().map(QueryFragments::reverse).toList(); } } - // Yeah, would be kinda nice having a simple method in Cypher-DSL ;) - private static SortItem reverse(SortItem sortItem) { - - var sortedExpression = new AtomicReference(); - var sortDirection = new AtomicReference(); - - sortItem.accept(segment -> { - if (segment instanceof SortItem.Direction direction) { - sortDirection.compareAndSet(null, direction == SortItem.Direction.UNDEFINED || direction == SortItem.Direction.ASC ? SortItem.Direction.DESC : SortItem.Direction.ASC); - } else if (segment instanceof Expression expression) { - sortedExpression.compareAndSet(null, expression); - } - }); - - // Default might not explicitly set. - sortDirection.compareAndSet(null, SortItem.Direction.DESC); - return Cypher.sort(sortedExpression.get(), sortDirection.get()); + public void setOrderBy(@Nullable Collection orderBy) { + this.orderBy = orderBy; } - @Nullable - public Number getLimit() { - return limit; + @Nullable public Number getLimit() { + return this.limit; } - @Nullable - public Long getSkip() { - return skip; + public void setLimit(Number limit) { + this.limit = limit; } + @Nullable public Long getSkip() { + return this.skip; + } + + public void setSkip(Long skip) { + this.skip = skip; + } /** * Describes which fields of an entity needs to get returned. */ - final static class ReturnTuple { + static final class ReturnTuple { + final NodeDescription nodeDescription; + final PropertyFilter filteredProperties; + final boolean isDistinct; + final List additionalExpressions; - private ReturnTuple(NodeDescription nodeDescription, Collection filteredProperties, boolean isDistinct, List additionalExpressions) { + private ReturnTuple(NodeDescription nodeDescription, + Collection filteredProperties, boolean isDistinct, + List additionalExpressions) { this.nodeDescription = nodeDescription; this.filteredProperties = PropertyFilter.from(filteredProperties, nodeDescription); this.isDistinct = isDistinct; @@ -243,13 +266,15 @@ public final class QueryFragments { } boolean include(PropertyFilter.RelaxedPropertyPath fieldName) { - String dotPath = nodeDescription.getGraphProperty(fieldName.getSegment()) - .filter(Neo4jPersistentProperty.class::isInstance) - .map(Neo4jPersistentProperty.class::cast) - .filter(p -> p.findAnnotation(Property.class) != null) - .map(p -> fieldName.toDotPath(p.getPropertyName())) - .orElseGet(fieldName::toDotPath); + String dotPath = this.nodeDescription.getGraphProperty(fieldName.getSegment()) + .filter(Neo4jPersistentProperty.class::isInstance) + .map(Neo4jPersistentProperty.class::cast) + .filter(p -> p.findAnnotation(Property.class) != null) + .map(p -> fieldName.toDotPath(p.getPropertyName())) + .orElseGet(fieldName::toDotPath); return this.filteredProperties.contains(dotPath, fieldName.getType()); } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index 4044de008..4c95f6bc0 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -15,6 +15,15 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + import org.apiguardian.api.API; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -24,6 +33,7 @@ import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.PatternElement; import org.neo4j.cypherdsl.core.RelationshipPattern; import org.neo4j.cypherdsl.core.SortItem; + import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -37,15 +47,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.PropertyFilter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Predicate; - import static org.neo4j.cypherdsl.core.Cypher.parameter; /** @@ -56,21 +57,28 @@ import static org.neo4j.cypherdsl.core.Cypher.parameter; */ @API(status = API.Status.INTERNAL, since = "6.0.4") public final class QueryFragmentsAndParameters { - private final static CypherGenerator cypherGenerator = CypherGenerator.INSTANCE; - private Map parameters; - @Nullable - private NodeDescription nodeDescription; + + private static final CypherGenerator cypherGenerator = CypherGenerator.INSTANCE; + private final QueryFragments queryFragments; + @Nullable private final String cypherQuery; + private final Sort sort; - public QueryFragmentsAndParameters(@Nullable NodeDescription nodeDescription, QueryFragments queryFragments, Map parameters, @Nullable Sort sort) { + private Map parameters; + + @Nullable + private NodeDescription nodeDescription; + + public QueryFragmentsAndParameters(@Nullable NodeDescription nodeDescription, QueryFragments queryFragments, + Map parameters, @Nullable Sort sort) { this.nodeDescription = nodeDescription; this.queryFragments = queryFragments; this.parameters = parameters; this.cypherQuery = null; - this.sort = sort == null ? Sort.unsorted() : sort; + this.sort = (sort != null) ? sort : Sort.unsorted(); } public QueryFragmentsAndParameters(@NonNull String cypherQuery) { @@ -84,32 +92,6 @@ public final class QueryFragmentsAndParameters { this.sort = Sort.unsorted(); } - public Map getParameters() { - return parameters; - } - - public QueryFragments getQueryFragments() { - return queryFragments; - } - - @Nullable - public String getCypherQuery() { - return cypherQuery; - } - - @Nullable - public NodeDescription getNodeDescription() { - return nodeDescription; - } - - public void setParameters(Map newParameters) { - this.parameters = newParameters; - } - - public Sort getSort() { - return sort; - } - /* * Convenience methods that are used by the (Reactive)Neo4jTemplate */ @@ -126,8 +108,10 @@ public final class QueryFragmentsAndParameters { Condition condition; var idProperty = entityMetaData.getIdProperty(); if (idProperty != null && idProperty.isComposite()) { - condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(idProperty, container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID)); - } else { + condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(idProperty, + container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID)); + } + else { condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)); } @@ -150,7 +134,8 @@ public final class QueryFragmentsAndParameters { args.add(container.property(key)); } condition = Cypher.mapOf(args.toArray()).in(parameter(Constants.NAME_OF_IDS)); - } else { + } + else { condition = entityMetaData.getIdExpression().in(parameter(Constants.NAME_OF_IDS)); } @@ -174,12 +159,15 @@ public final class QueryFragmentsAndParameters { QueryFragments queryFragments = forFindOrExistsById(entityMetaData); queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForExists(entityMetaData)); - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Objects.requireNonNullElseGet(parameters, Map::of), null); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, + Objects.requireNonNullElseGet(parameters, Map::of), null); } - public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, @Nullable Pageable pageable, @Nullable Sort sort) { + public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, + @Nullable Pageable pageable, @Nullable Sort sort) { - return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, null, null, null, Collections.emptyMap(), null, null, null); + return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, null, null, null, + Collections.emptyMap(), null, null, null); } /* @@ -189,94 +177,93 @@ public final class QueryFragmentsAndParameters { return forExample(mappingContext, example, null, null, null, null, null, null, null); } - static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example example, Pageable pageable, java.util.function.Predicate includeField) { + static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example example, + Pageable pageable, java.util.function.Predicate includeField) { return forExample(mappingContext, example, null, pageable, null, null, null, null, includeField); } - static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable Integer limit, java.util.function.Predicate includeField) { + static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example example, + Sort sort, @Nullable Integer limit, + java.util.function.Predicate includeField) { return forExample(mappingContext, example, null, null, sort, limit, null, null, includeField); } - static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, Example example, @Nullable Condition keysetScrollPositionCondition, Sort sort, Integer limit, Long skip, ScrollPosition scrollPosition, java.util.function.Predicate includeField) { - return forExample(mappingContext, example, keysetScrollPositionCondition, null, sort, limit, skip, scrollPosition, includeField); + static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, + Example example, @Nullable Condition keysetScrollPositionCondition, Sort sort, Integer limit, Long skip, + ScrollPosition scrollPosition, + java.util.function.Predicate includeField) { + return forExample(mappingContext, example, keysetScrollPositionCondition, null, sort, limit, skip, + scrollPosition, includeField); } private static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, - @Nullable Condition keysetScrollPositionCondition, - @Nullable Pageable pageable, - @Nullable Sort sort, - @Nullable Integer limit, - @Nullable Long skip, - @Nullable ScrollPosition scrollPosition, - @Nullable Predicate includeField - ) { + @Nullable Condition keysetScrollPositionCondition, @Nullable Pageable pageable, @Nullable Sort sort, + @Nullable Integer limit, @Nullable Long skip, @Nullable ScrollPosition scrollPosition, + @Nullable Predicate includeField) { var predicate = org.springframework.data.neo4j.repository.query.Predicate.create(mappingContext, example); Map parameters = predicate.getParameters(); Set propertyPathWrappers = predicate.getPropertyPathWrappers(); Condition condition = predicate.getCondition(); - Neo4jPersistentEntity persistentEntity = Objects - .requireNonNull(mappingContext.getPersistentEntity(example.getProbeType()), () -> "Could not load persistent entity for probe type %s".formatted(example.getProbeType())); + Neo4jPersistentEntity persistentEntity = Objects.requireNonNull( + mappingContext.getPersistentEntity(example.getProbeType()), + () -> "Could not load persistent entity for probe type %s".formatted(example.getProbeType())); if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { if (!keysetScrollPosition.isInitial()) { condition = condition.and(keysetScrollPositionCondition); } - QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(persistentEntity, pageable, - sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers); - queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); + QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(persistentEntity, + pageable, sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers); + queryFragmentsAndParameters.getQueryFragments() + .setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); return queryFragmentsAndParameters; } - return getQueryFragmentsAndParameters(persistentEntity, pageable, - sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers); + return getQueryFragmentsAndParameters(persistentEntity, pageable, sort, null, limit, skip, parameters, + condition, includeField, propertyPathWrappers); } /** - * Utility method for creating a query fragment including parameters for a given condition. - * - * @param entityMetaData The metadata of a given and known entity - * @param condition A Cypher-DSL condition - * @return Fully populated fragments and parameter + * Utility method for creating a query fragment including parameters for a given + * condition. + * @param entityMetaData the metadata of a given and known entity + * @param condition a Cypher-DSL condition + * @return fully populated fragments and parameter */ @API(status = API.Status.EXPERIMENTAL, since = "6.1.7") public static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - Condition condition) { + Condition condition) { return forCondition(entityMetaData, condition, null, null, null, null, null, null); } static QueryFragmentsAndParameters forConditionAndPageable(Neo4jPersistentEntity entityMetaData, - Condition condition, Pageable pageable, - @Nullable Predicate includeField) { + Condition condition, Pageable pageable, + @Nullable Predicate includeField) { return forCondition(entityMetaData, condition, pageable, null, null, null, null, includeField); } - static QueryFragmentsAndParameters forConditionAndSort(Neo4jPersistentEntity entityMetaData, Condition condition, Sort sort, @Nullable Integer limit, - @Nullable Predicate includeField) { + static QueryFragmentsAndParameters forConditionAndSort(Neo4jPersistentEntity entityMetaData, Condition condition, + Sort sort, @Nullable Integer limit, @Nullable Predicate includeField) { return forCondition(entityMetaData, condition, null, sort, null, limit, null, includeField); } - static QueryFragmentsAndParameters forConditionAndSortItems(Neo4jPersistentEntity entityMetaData, Condition condition, @Nullable Collection sortItems) { + static QueryFragmentsAndParameters forConditionAndSortItems(Neo4jPersistentEntity entityMetaData, + Condition condition, @Nullable Collection sortItems) { return forCondition(entityMetaData, condition, null, null, sortItems, null, null, null); } static QueryFragmentsAndParameters forConditionWithScrollPosition(Neo4jPersistentEntity entityMetaData, - Condition condition, - @Nullable Condition keysetCondition, - ScrollPosition scrollPosition, - Sort sort, - @Nullable Integer limit, - @Nullable Predicate includeField) { + Condition condition, @Nullable Condition keysetCondition, ScrollPosition scrollPosition, Sort sort, + @Nullable Integer limit, @Nullable Predicate includeField) { long skip = 0L; if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { - skip = offsetScrollPosition.isInitial() - ? 0 - : offsetScrollPosition.getOffset() + 1; + skip = offsetScrollPosition.isInitial() ? 0 : offsetScrollPosition.getOffset() + 1; return forCondition(entityMetaData, condition, null, sort, null, limit, skip, includeField); } @@ -285,71 +272,68 @@ public final class QueryFragmentsAndParameters { if (!scrollPosition.isInitial() && keysetCondition != null) { condition = condition.and(keysetCondition); } - QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(entityMetaData, null, - sort, null, limit, skip, Collections.emptyMap(), condition, includeField, null); - queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); + QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(entityMetaData, + null, sort, null, limit, skip, Collections.emptyMap(), condition, includeField, null); + queryFragmentsAndParameters.getQueryFragments() + .setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); return queryFragmentsAndParameters; } - throw new IllegalArgumentException("ScrollPosition must be of type OffsetScrollPosition or KeysetScrollPosition. Unexpected type %s found.".formatted(scrollPosition.getClass())); + throw new IllegalArgumentException( + "ScrollPosition must be of type OffsetScrollPosition or KeysetScrollPosition. Unexpected type %s found." + .formatted(scrollPosition.getClass())); } // Parameter re-ordering helper private static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - @Nullable Condition condition, - @Nullable Pageable pageable, - @Nullable Sort sort, - @Nullable Collection sortItems, - @Nullable Integer limit, - @Nullable Long skip, - @Nullable Predicate includeField - ) { + @Nullable Condition condition, @Nullable Pageable pageable, @Nullable Sort sort, + @Nullable Collection sortItems, @Nullable Integer limit, @Nullable Long skip, + @Nullable Predicate includeField) { - return getQueryFragmentsAndParameters(entityMetaData, pageable, sort, sortItems, limit, skip, Collections.emptyMap(), condition, includeField, null); + return getQueryFragmentsAndParameters(entityMetaData, pageable, sort, sortItems, limit, skip, + Collections.emptyMap(), condition, includeField, null); } - private static QueryFragmentsAndParameters getQueryFragmentsAndParameters( - Neo4jPersistentEntity entityMetaData, - @Nullable Pageable pageable, - @Nullable Sort sort, - @Nullable Collection sortItems, - @Nullable Integer limit, - @Nullable Long skip, - @Nullable Map parameters, - @Nullable Condition condition, - @Nullable Predicate includeField, - @Nullable Set propertyPathWrappers - ) { + private static QueryFragmentsAndParameters getQueryFragmentsAndParameters(Neo4jPersistentEntity entityMetaData, + @Nullable Pageable pageable, @Nullable Sort sort, @Nullable Collection sortItems, + @Nullable Integer limit, @Nullable Long skip, @Nullable Map parameters, + @Nullable Condition condition, @Nullable Predicate includeField, + @Nullable Set propertyPathWrappers) { QueryFragments queryFragments = new QueryFragments(); if (propertyPathWrappers != null && !propertyPathWrappers.isEmpty()) { Node startNode = Cypher.node(entityMetaData.getPrimaryLabel(), entityMetaData.getAdditionalLabels()) - .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)); + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)); List relationshipChain = new ArrayList<>(); for (PropertyPathWrapper possiblePathWithRelationship : propertyPathWrappers) { - relationshipChain.add((RelationshipPattern) possiblePathWithRelationship.createRelationshipChain(startNode)); + relationshipChain + .add((RelationshipPattern) possiblePathWithRelationship.createRelationshipChain(startNode)); } queryFragments.setMatchOn(relationshipChain); - } else { + } + else { queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); } queryFragments.setCondition(condition); if (includeField == null) { queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); - } else { - queryFragments.setReturnExpressions( - cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField)); + } + else { + queryFragments + .setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField)); queryFragments.setProjectingPropertyFilter(includeField); } if (pageable != null) { adaptPageable(entityMetaData, pageable, queryFragments); - } else { + } + else { if (sort != null) { queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, sort)); - } else if (sortItems != null) { + } + else if (sortItems != null) { queryFragments.setOrderBy(sortItems); } if (limit != null) { @@ -362,14 +346,12 @@ public final class QueryFragmentsAndParameters { } } - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Objects.requireNonNullElseGet(parameters, Map::of), sort); + return new QueryFragmentsAndParameters(entityMetaData, queryFragments, + Objects.requireNonNullElseGet(parameters, Map::of), sort); } - private static void adaptPageable( - Neo4jPersistentEntity entityMetaData, - Pageable pageable, - QueryFragments queryFragments - ) { + private static void adaptPageable(Neo4jPersistentEntity entityMetaData, Pageable pageable, + QueryFragments queryFragments) { if (pageable.isPaged()) { queryFragments.setSkip(pageable.getOffset()); queryFragments.setLimit(pageable.getPageSize()); @@ -380,4 +362,28 @@ public final class QueryFragmentsAndParameters { } } + public Map getParameters() { + return this.parameters; + } + + public void setParameters(Map newParameters) { + this.parameters = newParameters; + } + + public QueryFragments getQueryFragments() { + return this.queryFragments; + } + + @Nullable public String getCypherQuery() { + return this.cypherQuery; + } + + @Nullable public NodeDescription getNodeDescription() { + return this.nodeDescription; + } + + public Sort getSort() { + return this.sort; + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java index b9e7b6465..6dfa971d1 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java @@ -19,9 +19,12 @@ import java.util.Arrays; import java.util.Optional; import java.util.function.Function; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SortItem; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -34,25 +37,25 @@ import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Predicate; - /** - * Querydsl specific fragment for extending {@link org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository} - * with an implementation of {@link QuerydslPredicateExecutor}. Provides the necessary infrastructure for translating - * Query-DSL predicates into conditions that are passed along to the Cypher-DSL and eventually to the template infrastructure. - * This fragment will be loaded by the repository infrastructure when a repository is declared extending the above interface. + * Querydsl specific fragment for extending + * {@link org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository} with an + * implementation of {@link QuerydslPredicateExecutor}. Provides the necessary + * infrastructure for translating Query-DSL predicates into conditions that are passed + * along to the Cypher-DSL and eventually to the template infrastructure. This fragment + * will be loaded by the repository infrastructure when a repository is declared extending + * the above interface. * + * @param the returned domain type. * @author Michael J. Simons - * @param The returned domain type. - * @soundtrack Various - Chef Aid: The South Park Album * @since 6.1 */ @API(status = API.Status.INTERNAL, since = "6.1") public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicateExecutor { /** - * Non-fluent operations are translated directly into Cypherdsl conditions and executed elsewhere. + * Non-fluent operations are translated directly into Cypherdsl conditions and + * executed elsewhere. */ private final CypherdslConditionExecutor delegate; @@ -67,12 +70,12 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat private final Neo4jPersistentEntity metaData; /** - * Mapping context + * Mapping context. */ private final Neo4jMappingContext mappingContext; - public QuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation entityInformation, - Neo4jOperations neo4jOperations) { + public QuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, + Neo4jEntityInformation entityInformation, Neo4jOperations neo4jOperations) { this.mappingContext = mappingContext; this.delegate = new CypherdslConditionExecutorImpl<>(entityInformation, neo4jOperations); @@ -80,6 +83,15 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat this.metaData = entityInformation.getEntityMetaData(); } + static SortItem[] toSortItems(OrderSpecifier... orderSpecifiers) { + + return Arrays.stream(orderSpecifiers) + .map(os -> Cypher.sort(Cypher.adapt(os.getTarget()).asExpression(), + os.isAscending() ? SortItem.Direction.ASC : SortItem.Direction.DESC)) + .toArray(SortItem[]::new); + + } + @Override public Optional findOne(Predicate predicate) { @@ -122,14 +134,6 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat return this.delegate.count(Cypher.adapt(predicate).asCondition()); } - static SortItem[] toSortItems(OrderSpecifier... orderSpecifiers) { - - return Arrays.stream(orderSpecifiers) - .map(os -> Cypher.sort(Cypher.adapt(os.getTarget()).asExpression(), - os.isAscending() ? SortItem.Direction.ASC : SortItem.Direction.DESC)).toArray(SortItem[]::new); - - } - @Override public boolean exists(Predicate predicate) { return findAll(predicate).iterator().hasNext(); @@ -139,13 +143,15 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat public R findBy(Predicate predicate, Function, R> queryFunction) { if (this.neo4jOperations instanceof FluentFindOperation ops) { - @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. - FetchableFluentQuery fluentQuery = - (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(), - ops, this::count, this::exists); + @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S + // and at this stage, the same. + FetchableFluentQuery fluentQuery = (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>( + predicate, this.mappingContext, this.metaData, this.metaData.getType(), ops, this::count, + this::exists); return queryFunction.apply(fluentQuery); } throw new UnsupportedOperationException( "Fluent find by predicate not supported with standard Neo4jOperations, must support fluent queries too"); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java index 38d8b1c95..5024a43bf 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslBasedQuery.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -34,34 +35,36 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.util.Assert; /** - * A repository query based on the Cypher-DSL. This variant has been introduced as it turns out to be rather hard to access - * facts about the returned type or the returned projection (if any). + * A repository query based on the Cypher-DSL. This variant has been introduced as it + * turns out to be rather hard to access facts about the returned type or the returned + * projection (if any). * * @author Michael J. Simons * @since 6.1 */ final class ReactiveCypherdslBasedQuery extends AbstractReactiveNeo4jQuery { - static ReactiveCypherdslBasedQuery create(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - Neo4jQueryMethod queryMethod, ProjectionFactory projectionFactory, Function renderer) { - - return new ReactiveCypherdslBasedQuery(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.DEFAULT, projectionFactory, renderer); - } - private final Function renderer; - private ReactiveCypherdslBasedQuery(ReactiveNeo4jOperations neo4jOperations, - Neo4jMappingContext mappingContext, + private ReactiveCypherdslBasedQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, Neo4jQueryType queryType, ProjectionFactory projectionFactory, Function renderer) { super(neo4jOperations, mappingContext, queryMethod, queryType, projectionFactory); this.renderer = renderer; } + static ReactiveCypherdslBasedQuery create(ReactiveNeo4jOperations neo4jOperations, + Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, ProjectionFactory projectionFactory, + Function renderer) { + + return new ReactiveCypherdslBasedQuery(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.DEFAULT, + projectionFactory, renderer); + } + @Override - protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - Supplier> mappingFunction, + protected PreparedQuery prepareQuery(Class returnedType, + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, UnaryOperator limitModifier) { Object[] parameters = parameterAccessor.getValues(); @@ -73,9 +76,10 @@ final class ReactiveCypherdslBasedQuery extends AbstractReactiveNeo4jQuery { Map boundParameters = statement.getCatalog().getParameters(); return PreparedQuery.queryFor(returnedType) - .withCypherQuery(renderer.apply(statement)) - .withParameters(boundParameters) - .usingMappingFunction(mappingFunction) - .build(); + .withCypherQuery(this.renderer.apply(statement)) + .withParameters(boundParameters) + .usingMappingFunction(mappingFunction) + .build(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java index 39b2553db..820f67118 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; - import java.util.Arrays; import java.util.function.Predicate; @@ -25,21 +23,25 @@ import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.Statement; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.PropertyFilter; -import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; +import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; /** + * Implementation of the {@link ReactiveCypherdslConditionExecutor}. + * + * @param the returned domain type * @author Niklas Krieger * @author Michael J. Simons - * @param The returned domain type. * @since 6.3.3 */ @API(status = API.Status.INTERNAL, since = "6.3.3") @@ -62,59 +64,57 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive @Override public Mono findOne(Condition condition) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition) - ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition)) + .flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); } @Override public Flux findAll(Condition condition) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition) - ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Flux findAll(Condition condition, Sort sort) { Predicate noFilter = PropertyFilter.NO_FILTER; - return this.neo4jOperations.toExecutableQuery( - metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSort( - this.metaData, condition, sort, null, noFilter - ) - ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSort(this.metaData, condition, sort, null, noFilter)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Flux findAll(Condition condition, SortItem... sortItems) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSortItems( - this.metaData, condition, Arrays.asList(sortItems) - ) - ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, + Arrays.asList(sortItems))) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Flux findAll(SortItem... sortItems) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Cypher.noCondition(), - Arrays.asList(sortItems)) - ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Cypher.noCondition(), + Arrays.asList(sortItems))) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Mono count(Condition condition) { Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, condition) - .returning(Cypher.count(asterisk())).build(); + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, statement.getCatalog().getParameters()); } @@ -122,4 +122,5 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive public Mono exists(Condition condition) { return count(condition).map(count -> count > 0); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java index 5a023c52e..c1daf116d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java @@ -21,6 +21,9 @@ import java.util.function.Function; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -35,17 +38,14 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - /** - * Immutable implementation of a {@link ReactiveFluentQuery}. All - * methods that return a {@link ReactiveFluentQuery} return a new instance, the original instance won't be + * Immutable implementation of a {@link ReactiveFluentQuery}. All methods that return a + * {@link ReactiveFluentQuery} return a new instance, the original instance won't be * modified. * + * @param the source type + * @param the result type * @author Michael J. Simons - * @param Source type - * @param Result type * @since 6.2 */ @API(status = API.Status.INTERNAL, since = "6.2") @@ -61,29 +61,17 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp private final Function, Mono> existsOperation; - ReactiveFluentQueryByExample( - Example example, - Class resultType, - Neo4jMappingContext mappingContext, - ReactiveFluentFindOperation findOperation, - Function, Mono> countOperation, - Function, Mono> existsOperation - ) { - this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), - null, null); + ReactiveFluentQueryByExample(Example example, Class resultType, Neo4jMappingContext mappingContext, + ReactiveFluentFindOperation findOperation, Function, Mono> countOperation, + Function, Mono> existsOperation) { + this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), null, + null); } - ReactiveFluentQueryByExample( - Example example, - Class resultType, - Neo4jMappingContext mappingContext, - ReactiveFluentFindOperation findOperation, - Function, Mono> countOperation, - Function, Mono> existsOperation, - Sort sort, - @Nullable Integer limit, - @Nullable Collection properties - ) { + ReactiveFluentQueryByExample(Example example, Class resultType, Neo4jMappingContext mappingContext, + ReactiveFluentFindOperation findOperation, Function, Mono> countOperation, + Function, Mono> existsOperation, Sort sort, @Nullable Integer limit, + @Nullable Collection properties) { super(resultType, sort, limit, properties); this.mappingContext = mappingContext; this.example = example; @@ -96,16 +84,17 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp @SuppressWarnings("HiddenField") public ReactiveFluentQuery sortBy(Sort sort) { - return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, + this.properties); } @Override @SuppressWarnings("HiddenField") public ReactiveFluentQuery limit(int limit) { - return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, limit, this.properties); + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @@ -120,18 +109,19 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp @SuppressWarnings("HiddenField") public ReactiveFluentQuery project(Collection properties) { - return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, + this.findOperation, this.countOperation, this.existsOperation, this.sort, this.limit, + mergeProperties(extractAllPaths(properties))); } @Override public Mono one() { - return findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, - createIncludedFieldsPredicate())) - .one(); + return this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, this.example, this.sort, + this.limit, createIncludedFieldsPredicate())) + .one(); } @Override @@ -143,56 +133,58 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp @Override public Flux all() { - return findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, - createIncludedFieldsPredicate())) - .all(); + return this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, this.example, this.sort, + this.limit, createIncludedFieldsPredicate())) + .all(); } @Override public Mono> page(Pageable pageable) { - Flux results = findOperation.find(example.getProbeType()) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, - createIncludedFieldsPredicate())) - .all(); - return results.collectList().zipWith(countOperation.apply(example)).map(tuple -> { - Page page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2()); - return page; - }); + Flux results = this.findOperation.find(this.example.getProbeType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithPageable(this.mappingContext, this.example, pageable, + createIncludedFieldsPredicate())) + .all(); + return results.collectList() + .zipWith(this.countOperation.apply(this.example)) + .map(tuple -> PageableExecutionUtils.getPage(tuple.getT1(), pageable, tuple::getT2)); } @Override public Mono> scroll(ScrollPosition scrollPosition) { Class domainType = this.example.getProbeType(); - Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainType); + Neo4jPersistentEntity entity = this.mappingContext.getRequiredPersistentEntity(domainType); - var skip = scrollPosition.isInitial() - ? 0 - : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + 1 - : 0; + var skip = scrollPosition.isInitial() ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) + ? offsetScrollPosition.getOffset() + 1 : 0; - Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getRequiredPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + Condition condition = (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) ? CypherAdapterUtils + .combineKeysetIntoCondition(this.mappingContext.getRequiredPersistentEntity(this.example.getProbeType()), + keysetScrollPosition, this.sort, this.mappingContext.getConversionService()) : null; - return findOperation.find(domainType) - .as(resultType) - .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate())) - .all() - .collectList() - .map(rawResult -> scroll(scrollPosition, rawResult, entity)); + return this.findOperation.find(domainType) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(this.mappingContext, this.example, + condition, this.sort, (this.limit != null) ? this.limit + 1 : 1, skip, scrollPosition, + createIncludedFieldsPredicate())) + .all() + .collectList() + .map(rawResult -> scroll(scrollPosition, rawResult, entity)); } @Override public Mono count() { - return countOperation.apply(example); + return this.countOperation.apply(this.example); } @Override public Mono exists() { - return existsOperation.apply(example); + return this.existsOperation.apply(this.example); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java index 3d1828d7e..749139f3d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -15,41 +15,40 @@ */ package org.springframework.data.neo4j.repository.query; -import org.jspecify.annotations.Nullable; -import org.springframework.data.domain.KeysetScrollPosition; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Window; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Collection; import java.util.function.Function; +import com.querydsl.core.types.Predicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Cypher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import com.querydsl.core.types.Predicate; - /** - * Immutable implementation of a {@link ReactiveFluentQuery}. All - * methods that return a {@link ReactiveFluentQuery} return a new instance, the original instance won't be + * Immutable implementation of a {@link ReactiveFluentQuery}. All methods that return a + * {@link ReactiveFluentQuery} return a new instance, the original instance won't be * modified. * + * @param the source type + * @param the result type * @author Michael J. Simons - * @param Source type - * @param Result type * @since 6.2 */ -@API(status = API.Status.INTERNAL, since = "6.2") final class ReactiveFluentQueryByPredicate - extends FluentQuerySupport implements ReactiveFluentQuery { +@API(status = API.Status.INTERNAL, since = "6.2") +final class ReactiveFluentQueryByPredicate extends FluentQuerySupport implements ReactiveFluentQuery { private final Predicate predicate; @@ -63,30 +62,17 @@ import com.querydsl.core.types.Predicate; private final Neo4jMappingContext mappingContext; - ReactiveFluentQueryByPredicate( - Predicate predicate, - Neo4jMappingContext mappingContext, - Neo4jPersistentEntity metaData, - Class resultType, - ReactiveFluentFindOperation findOperation, - Function> countOperation, - Function> existsOperation - ) { - this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null); + ReactiveFluentQueryByPredicate(Predicate predicate, Neo4jMappingContext mappingContext, + Neo4jPersistentEntity metaData, Class resultType, ReactiveFluentFindOperation findOperation, + Function> countOperation, Function> existsOperation) { + this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, + Sort.unsorted(), null, null); } - ReactiveFluentQueryByPredicate( - Predicate predicate, - Neo4jMappingContext mappingContext, - Neo4jPersistentEntity metaData, - Class resultType, - ReactiveFluentFindOperation findOperation, - Function> countOperation, - Function> existsOperation, - Sort sort, - @Nullable Integer limit, - @Nullable Collection properties - ) { + ReactiveFluentQueryByPredicate(Predicate predicate, Neo4jMappingContext mappingContext, + Neo4jPersistentEntity metaData, Class resultType, ReactiveFluentFindOperation findOperation, + Function> countOperation, Function> existsOperation, + Sort sort, @Nullable Integer limit, @Nullable Collection properties) { super(resultType, sort, limit, properties); this.predicate = predicate; this.mappingContext = mappingContext; @@ -100,45 +86,43 @@ import com.querydsl.core.types.Predicate; @SuppressWarnings("HiddenField") public ReactiveFluentQuery sortBy(Sort sort) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, + this.findOperation, this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, + this.properties); } @Override @SuppressWarnings("HiddenField") public ReactiveFluentQuery limit(int limit) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, limit, this.properties); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, + this.findOperation, this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @SuppressWarnings("HiddenField") public ReactiveFluentQuery as(Class resultType) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, - this.countOperation, this.existsOperation); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, + this.findOperation, this.countOperation, this.existsOperation); } @Override @SuppressWarnings("HiddenField") public ReactiveFluentQuery project(Collection properties) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, + this.findOperation, this.countOperation, this.existsOperation, this.sort, this.limit, + mergeProperties(extractAllPaths(properties))); } @Override public Mono one() { - return findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndSort(metaData, - Cypher.adapt(predicate).asCondition(), - sort, - limit, - createIncludedFieldsPredicate())) - .one(); + return this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndSort(this.metaData, + Cypher.adapt(this.predicate).asCondition(), this.sort, this.limit, createIncludedFieldsPredicate())) + .one(); } @Override @@ -150,61 +134,54 @@ import com.querydsl.core.types.Predicate; @Override public Flux all() { - return findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndSort(metaData, - Cypher.adapt(predicate).asCondition(), - sort, - limit, - createIncludedFieldsPredicate())) - .all(); + return this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndSort(this.metaData, + Cypher.adapt(this.predicate).asCondition(), this.sort, this.limit, createIncludedFieldsPredicate())) + .all(); } @Override public Mono> page(Pageable pageable) { - Flux results = findOperation.find(metaData.getType()) - .as(resultType) - .matching( - QueryFragmentsAndParameters.forConditionAndPageable(metaData, - Cypher.adapt(predicate).asCondition(), - pageable, - createIncludedFieldsPredicate())) - .all(); + Flux results = this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, + Cypher.adapt(this.predicate).asCondition(), pageable, createIncludedFieldsPredicate())) + .all(); - return results.collectList().zipWith(countOperation.apply(predicate)).map(tuple -> { - Page page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2()); - return page; - }); + return results.collectList() + .zipWith(this.countOperation.apply(this.predicate)) + .map(tuple -> PageableExecutionUtils.getPage(tuple.getT1(), pageable, tuple::getT2)); } @Override public Mono> scroll(ScrollPosition scrollPosition) { - QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData, - Cypher.adapt(predicate).asCondition(), - (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition - ? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService()) - : null), - scrollPosition, sort, - limit == null ? 1 : limit + 1, - createIncludedFieldsPredicate()); + QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters + .forConditionWithScrollPosition(this.metaData, Cypher.adapt(this.predicate).asCondition(), + (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) + ? CypherAdapterUtils.combineKeysetIntoCondition(this.metaData, keysetScrollPosition, + this.sort, this.mappingContext.getConversionService()) + : null, + scrollPosition, this.sort, (this.limit != null) ? this.limit + 1 : 1, + createIncludedFieldsPredicate()); - return findOperation.find(metaData.getType()) - .as(resultType) - .matching(queryFragmentsAndParameters) - .all() - .collectList() - .map(rawResult -> scroll(scrollPosition, rawResult, metaData)); + return this.findOperation.find(this.metaData.getType()) + .as(this.resultType) + .matching(queryFragmentsAndParameters) + .all() + .collectList() + .map(rawResult -> scroll(scrollPosition, rawResult, this.metaData)); } @Override public Mono count() { - return countOperation.apply(predicate); + return this.countOperation.apply(this.predicate); } @Override public Mono exists() { - return existsOperation.apply(predicate); + return this.existsOperation.apply(this.predicate); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java index 4b0b1f50a..59b488565 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; + import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -40,8 +41,11 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrategy { private final ReactiveNeo4jOperations neo4jOperations; + private final Neo4jMappingContext mappingContext; + private final ValueExpressionDelegate delegate; + private final Configuration configuration; public ReactiveNeo4jQueryLookupStrategy(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, @@ -52,9 +56,6 @@ public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrate this.configuration = configuration; } - /* (non-Javadoc) - * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) - */ @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, NamedQueries namedQueries) { @@ -63,15 +64,21 @@ public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrate String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { - return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, + return ReactiveStringBasedNeo4jQuery.create(this.neo4jOperations, this.mappingContext, this.delegate, queryMethod, namedQueries.getQuery(namedQueryName), projectionFactory); - } else if (queryMethod.hasQueryAnnotation()) { - return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, + } + else if (queryMethod.hasQueryAnnotation()) { + return ReactiveStringBasedNeo4jQuery.create(this.neo4jOperations, this.mappingContext, this.delegate, queryMethod, projectionFactory); - } else if (queryMethod.isCypherBasedProjection()) { - return ReactiveCypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, projectionFactory, Renderer.getRenderer(configuration)::render); - } else { - return ReactivePartTreeNeo4jQuery.create(neo4jOperations, mappingContext, queryMethod, projectionFactory); + } + else if (queryMethod.isCypherBasedProjection()) { + return ReactiveCypherdslBasedQuery.create(this.neo4jOperations, this.mappingContext, queryMethod, + projectionFactory, Renderer.getRenderer(this.configuration)::render); + } + else { + return ReactivePartTreeNeo4jQuery.create(this.neo4jOperations, this.mappingContext, queryMethod, + projectionFactory); } } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java index 0b1be5f3a..efaaad052 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryMethod.java @@ -31,10 +31,11 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; /** - * This is unfortunately a little bit of a hack to provide the information that the returned types by this query are - * always considered as stream. We try to either separate imperative and reactive concerns but due to type compatibility - * we extend the {@link Neo4jQueryMethod} here instead of creating a complete new reactive focused logical branch. It - * would only contain duplications of several classes. + * This is unfortunately a little bit of a hack to provide the information that the + * returned types by this query are always considered as stream. We try to either separate + * imperative and reactive concerns but due to type compatibility we extend the + * {@link Neo4jQueryMethod} here instead of creating a complete new reactive focused + * logical branch. It would only contain duplications of several classes. * * @author Gerrit Meier * @author Mark Paluch @@ -44,6 +45,7 @@ final class ReactiveNeo4jQueryMethod extends Neo4jQueryMethod { @SuppressWarnings("rawtypes") private static final TypeInformation PAGE_TYPE = TypeInformation.of(Page.class); + @SuppressWarnings("rawtypes") private static final TypeInformation SLICE_TYPE = TypeInformation.of(Slice.class); @@ -51,7 +53,6 @@ final class ReactiveNeo4jQueryMethod extends Neo4jQueryMethod { /** * Creates a new {@link ReactiveNeo4jQueryMethod} from the given parameters. - * * @param method must not be {@literal null}. * @param metadata must not be {@literal null}. * @param factory must not be {@literal null}. @@ -69,34 +70,31 @@ final class ReactiveNeo4jQueryMethod extends Neo4jQueryMethod { || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); if (singleWrapperWithWrappedPageableResult) { - throw new InvalidDataAccessApiUsageException( - String.format("'%s.%s' must not use sliced or paged execution, please use Flux.buffer(size, skip)", - ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + throw new InvalidDataAccessApiUsageException(String.format( + "'%s.%s' must not use sliced or paged execution, please use Flux.buffer(size, skip)", + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); } if (!multiWrapper) { throw new IllegalStateException(String.format( - "Method has to use a multi-item reactive wrapper return type. Offending method: %s", method.toString())); + "Method has to use a multi-item reactive wrapper return type. Offending method: %s", + method.toString())); } } this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery()) - && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType())) || super.isCollectionQuery()); + && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType())) + || super.isCollectionQuery()); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery() - */ @Override public boolean isCollectionQuery() { - return isCollectionQuery.get(); + return this.isCollectionQuery.get(); } /** * Always return {@literal true} to skip {@link Pageable} validation in * {@link org.springframework.data.repository.query.QueryMethod#QueryMethod(Method, RepositoryMetadata, ProjectionFactory)}. - * * @return always {@literal true}. */ @Override @@ -105,10 +103,12 @@ final class ReactiveNeo4jQueryMethod extends Neo4jQueryMethod { } /** - * Consider only {@link #isCollectionQuery()} as {@link java.util.stream.Stream} query isn't applicable here. + * Consider only {@link #isCollectionQuery()} as {@link java.util.stream.Stream} query + * isn't applicable here. */ @Override boolean isCollectionLikeQuery() { return isCollectionQuery(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java index 62f16cedc..c074690a3 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -24,6 +24,7 @@ import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -44,12 +45,6 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { private final PartTree tree; - public static RepositoryQuery create(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - Neo4jQueryMethod queryMethod, ProjectionFactory factory) { - return new ReactivePartTreeNeo4jQuery(neo4jOperations, mappingContext, queryMethod, - new PartTree(queryMethod.getName(), getDomainType(queryMethod)), factory); - } - private ReactivePartTreeNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, PartTree tree, ProjectionFactory factory) { super(neo4jOperations, mappingContext, queryMethod, Neo4jQueryType.fromPartTree(tree), factory); @@ -60,18 +55,29 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { this.tree.flatMap(OrPart::stream).forEach(validator::validatePart); } - @Override - protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - Supplier> mappingFunction, UnaryOperator limitModifier) { + static RepositoryQuery create(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, + Neo4jQueryMethod queryMethod, ProjectionFactory factory) { + return new ReactivePartTreeNeo4jQuery(neo4jOperations, mappingContext, queryMethod, + new PartTree(queryMethod.getName(), getDomainType(queryMethod)), factory); + } - CypherQueryCreator queryCreator = new CypherQueryCreator(mappingContext, queryMethod, getDomainType(queryMethod), - Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(tree)), tree, parameterAccessor, - includedProperties, this::convertParameter, limitModifier); + @Override + protected PreparedQuery prepareQuery(Class returnedType, + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, Supplier> mappingFunction, + UnaryOperator limitModifier) { + + CypherQueryCreator queryCreator = new CypherQueryCreator(this.mappingContext, this.queryMethod, + getDomainType(this.queryMethod), + Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(this.tree)), this.tree, + parameterAccessor, includedProperties, this::convertParameter, limitModifier); QueryFragmentsAndParameters queryAndParameters = queryCreator.createQuery(); - return PreparedQuery.queryFor(returnedType).withQueryFragmentsAndParameters(queryAndParameters) - .usingMappingFunction(mappingFunction).build(); + return PreparedQuery.queryFor(returnedType) + .withQueryFragmentsAndParameters(queryAndParameters) + .usingMappingFunction(mappingFunction) + .build(); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java index 345decda7..29c539e17 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java @@ -15,43 +15,45 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.asterisk; - -import org.jspecify.annotations.Nullable; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.Arrays; import java.util.Collection; import java.util.function.Function; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SortItem; import org.neo4j.cypherdsl.core.Statement; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.CypherGenerator; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Predicate; +import static org.neo4j.cypherdsl.core.Cypher.asterisk; /** - * Querydsl specific fragment for extending {@link org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository} - * with an implementation of {@link ReactiveQuerydslPredicateExecutor}. Provides the necessary infrastructure for translating - * Query-DSL predicates into conditions that are passed along to the Cypher-DSL and eventually to the template infrastructure. - * This fragment will be loaded by the repository infrastructure when a repository is declared extending the above interface. + * Querydsl specific fragment for extending + * {@link org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository} + * with an implementation of {@link ReactiveQuerydslPredicateExecutor}. Provides the + * necessary infrastructure for translating Query-DSL predicates into conditions that are + * passed along to the Cypher-DSL and eventually to the template infrastructure. This + * fragment will be loaded by the repository infrastructure when a repository is declared + * extending the above interface. * + * @param the returned domain type. * @author Michael J. Simons - * @param The returned domain type. * @since 6.2 */ @API(status = API.Status.INTERNAL, since = "6.2") @@ -64,12 +66,12 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive private final Neo4jPersistentEntity metaData; /** - * Mapping context + * Mapping context. */ private final Neo4jMappingContext mappingContext; - public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation entityInformation, - ReactiveNeo4jOperations neo4jOperations) { + public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, + Neo4jEntityInformation entityInformation, ReactiveNeo4jOperations neo4jOperations) { this.mappingContext = mappingContext; this.entityInformation = entityInformation; @@ -80,10 +82,10 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive @Override public Mono findOne(Predicate predicate) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition()) - ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition())) + .flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); } @Override @@ -100,7 +102,8 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive @Override public Flux findAll(Predicate predicate, OrderSpecifier... orders) { - return doFindAll(Cypher.adapt(predicate).asCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); + return doFindAll(Cypher.adapt(predicate).asCondition(), + Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); } @@ -111,19 +114,19 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive } private Flux doFindAll(Condition condition, @Nullable Collection sortItems) { - return this.neo4jOperations.toExecutableQuery( - this.metaData.getType(), - QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, - sortItems) - ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.metaData.getType(), + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, sortItems)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Mono count(Predicate predicate) { - Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, - Cypher.adapt(predicate).asCondition()) - .returning(Cypher.count(asterisk())).build(); + Statement statement = CypherGenerator.INSTANCE + .prepareMatchOf(this.metaData, Cypher.adapt(predicate).asCondition()) + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, statement.getCatalog().getParameters()); } @@ -133,15 +136,19 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive } @Override - public > P findBy(Predicate predicate, Function, P> queryFunction) { + public > P findBy(Predicate predicate, + Function, P> queryFunction) { if (this.neo4jOperations instanceof ReactiveFluentFindOperation ops) { - @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. - ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(), - ops, this::count, this::exists); + @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S + // and at this stage, the same. + ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>( + predicate, this.mappingContext, this.metaData, this.metaData.getType(), ops, this::count, + this::exists); return queryFunction.apply(fluentQuery); } throw new UnsupportedOperationException( "Fluent find by example not supported with standard Neo4jOperations, must support fluent queries too"); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java index 0d4978bd8..ef643a610 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -26,6 +26,7 @@ import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; @@ -41,15 +42,16 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Implementation of {@link RepositoryQuery} for query methods annotated with {@link Query @Query}. The flow to handle - * queries with SpEL parameters is as follows + * Implementation of {@link RepositoryQuery} for query methods annotated with + * {@link Query @Query}. The flow to handle queries with SpEL parameters is as follows *
    *
  1. Parse template as something that has SpEL-expressions in it
  2. *
  3. Replace the SpEL-expressions with Neo4j Statement template parameters
  4. - *
  5. The parameters passed here _and_ the values that might have been computed during SpEL-parsing
  6. + *
  7. The parameters passed here _and_ the values that might have been computed during + * SpEL-parsing
  8. *
- * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL expressions with real Neo4j - * parameters. + * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL + * expressions with real Neo4j parameters. * * @author Gerrit Meier * @author Michael J. Simons @@ -61,39 +63,51 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { private final ValueExpressionQueryRewriter.QueryExpressionEvaluator parsedQuery; + private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, + Neo4jQueryType queryType, ProjectionFactory factory) { + + super(neo4jOperations, mappingContext, queryMethod, queryType, factory); + + this.queryRewriter = createQueryRewriter(delegate); + this.parsedQuery = this.queryRewriter.parse(cypherTemplate, queryMethod.getParameters()); + } + /** - * Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The - * annotation is expected to have a value. - * + * Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated + * with {@link Query @Query}. The annotation is expected to have a value. * @param neo4jOperations reactive Neo4j operations * @param mappingContext a Neo4jMappingContext instance * @param delegate a ValueExpressionDelegate instance * @param queryMethod the query method - * @return A new instance of a String based Neo4j query. + * @param factory the projection factory to work with + * @return a new instance of a String based Neo4j query */ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, ProjectionFactory factory) { Query queryAnnotation = queryMethod.getQueryAnnotation() - .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method")); + .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method")); - String cypherTemplate = Optional.ofNullable(queryAnnotation.value()).filter(StringUtils::hasText) - .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not")); + String cypherTemplate = Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not")); return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, cypherTemplate, Neo4jQueryType.fromDefinition(queryAnnotation), factory); } /** - * Create a {@link ReactiveStringBasedNeo4jQuery} based on an explicit Cypher template. - * + * Create a {@link ReactiveStringBasedNeo4jQuery} based on an explicit Cypher + * template. * @param neo4jOperations reactive Neo4j operations * @param mappingContext a Neo4jMappingContext instance * @param delegate a ValueExpressionDelegate instance * @param queryMethod the query method - * @param cypherTemplate The template to use. - * @return A new instance of a String based Neo4j query. + * @param cypherTemplate the template to use. + * @param factory the projection factory to work with + * @return a new instance of a String based Neo4j query. */ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, @@ -105,17 +119,8 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { Neo4jQueryType.DEFAULT, factory); } - private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, Neo4jQueryType queryType, - ProjectionFactory factory) { - - super(neo4jOperations, mappingContext, queryMethod, queryType, factory); - - this.queryRewriter = createQueryRewriter(delegate); - this.parsedQuery = queryRewriter.parse(cypherTemplate, queryMethod.getParameters()); - } - - static ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter createQueryRewriter(ValueExpressionDelegate delegate) { + static ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter createQueryRewriter( + ValueExpressionDelegate delegate) { return ValueExpressionQueryRewriter.of(delegate, StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource); } @@ -127,13 +132,17 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { UnaryOperator limitModifier) { Map boundParameters = bindParameters(parameterAccessor); - QueryContext queryContext = new QueryContext(queryMethod.getRepositoryName() + "." + queryMethod.getName(), - parsedQuery.getQueryString(), boundParameters); + QueryContext queryContext = new QueryContext( + this.queryMethod.getRepositoryName() + "." + this.queryMethod.getName(), + this.parsedQuery.getQueryString(), boundParameters); logWarningsIfNecessary(queryContext, parameterAccessor); - return PreparedQuery.queryFor(returnedType).withCypherQuery(queryContext.query).withParameters(boundParameters) - .usingMappingFunction(mappingFunction).build(); + return PreparedQuery.queryFor(returnedType) + .withCypherQuery(queryContext.query) + .withParameters(boundParameters) + .usingMappingFunction(mappingFunction) + .build(); } Map bindParameters(Neo4jParameterAccessor parameterAccessor) { @@ -142,7 +151,8 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { Map resolvedParameters = new HashMap<>(); // Values from the parameter accessor can only get converted after evaluation - for (Map.Entry evaluatedParam : parsedQuery.evaluate(parameterAccessor.getValues()).entrySet()) { + for (Map.Entry evaluatedParam : this.parsedQuery.evaluate(parameterAccessor.getValues()) + .entrySet()) { Object value = evaluatedParam.getValue(); if (!(evaluatedParam.getValue() instanceof Neo4jSpelSupport.LiteralReplacement)) { Neo4jQuerySupport.logParameterIfNull(evaluatedParam.getKey(), value); @@ -166,4 +176,5 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { return resolvedParameters; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java index 7c7d50dc2..f55a208f7 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java @@ -15,10 +15,15 @@ */ package org.springframework.data.neo4j.repository.query; -import org.apiguardian.api.API; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.LongSupplier; +import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Statement; + import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -32,19 +37,14 @@ import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuer import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.support.PageableExecutionUtils; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.LongSupplier; - import static org.neo4j.cypherdsl.core.Cypher.asterisk; /** * A fragment for repositories providing "Query by example" functionality. * + * @param type of the domain class * @author Michael J. Simons * @author Ján Šúr - * @param type of the domain class * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -65,27 +65,37 @@ public final class SimpleQueryByExampleExecutor implements QueryByExampleExec @Override public Optional findOne(Example example) { - return this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExample(mappingContext, example)).getSingleResult(); + return this.neo4jOperations + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExample(this.mappingContext, example)) + .getSingleResult(); } @Override public List findAll(Example example) { - return this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExample(mappingContext, example)).getResults(); + return this.neo4jOperations + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExample(this.mappingContext, example)) + .getResults(); } @Override public List findAll(Example example, Sort sort) { - return this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER)).getResults(); + return this.neo4jOperations + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, example, sort, null, + PropertyFilter.NO_FILTER)) + .getResults(); } @Override public Page findAll(Example example, Pageable pageable) { - List page = this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, PropertyFilter.NO_FILTER)).getResults(); + List page = this.neo4jOperations + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExampleWithPageable(this.mappingContext, example, pageable, + PropertyFilter.NO_FILTER)) + .getResults(); LongSupplier totalCountSupplier = () -> this.count(example); return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); @@ -94,9 +104,10 @@ public final class SimpleQueryByExampleExecutor implements QueryByExampleExec @Override public long count(Example example) { - Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.useWithReadingFragment(cypherGenerator::prepareMatchOf) - .returning(Cypher.count(asterisk())).build(); + Predicate predicate = Predicate.create(this.mappingContext, example); + Statement statement = predicate.useWithReadingFragment(this.cypherGenerator::prepareMatchOf) + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, predicate.getParameters()); } @@ -104,9 +115,10 @@ public final class SimpleQueryByExampleExecutor implements QueryByExampleExec @Override public boolean exists(Example example) { - Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.useWithReadingFragment(cypherGenerator::prepareMatchOf) - .returning(Cypher.count(asterisk())).build(); + Predicate predicate = Predicate.create(this.mappingContext, example); + Statement statement = predicate.useWithReadingFragment(this.cypherGenerator::prepareMatchOf) + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, predicate.getParameters()) > 0; } @@ -116,10 +128,11 @@ public final class SimpleQueryByExampleExecutor implements QueryByExampleExec if (this.neo4jOperations instanceof FluentFindOperation ops) { FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, example.getProbeType(), - mappingContext, ops, this::count, this::exists); + this.mappingContext, ops, this::count, this::exists); return queryFunction.apply(fluentQuery); } throw new UnsupportedOperationException( "Fluent find by example not supported with standard Neo4jOperations, must support fluent queries too"); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java index 94027a678..5783cadf2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java @@ -15,11 +15,15 @@ */ package org.springframework.data.neo4j.repository.query; -import org.apiguardian.api.API; +import java.util.function.Function; +import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Statement; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; @@ -29,19 +33,16 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import static org.neo4j.cypherdsl.core.Cypher.asterisk; -import java.util.function.Function; - /** - * A fragment for repositories providing "Query by example" functionality in a reactive way. + * A fragment for repositories providing "Query by example" functionality in a reactive + * way. * + * @param type of the domain class * @author Gerrit Meier * @author Michael J. Simons - * @param type of the domain class * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -53,7 +54,8 @@ public final class SimpleReactiveQueryByExampleExecutor implements ReactiveQu private final CypherGenerator cypherGenerator; - public SimpleReactiveQueryByExampleExecutor(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext) { + public SimpleReactiveQueryByExampleExecutor(ReactiveNeo4jOperations neo4jOperations, + Neo4jMappingContext mappingContext) { this.neo4jOperations = neo4jOperations; this.mappingContext = mappingContext; @@ -63,30 +65,35 @@ public final class SimpleReactiveQueryByExampleExecutor implements ReactiveQu @Override public Mono findOne(Example example) { return this.neo4jOperations - .toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExample(mappingContext, example)) - .flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExample(this.mappingContext, example)) + .flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); } @Override public Flux findAll(Example example) { return this.neo4jOperations - .toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExample(mappingContext, example)) - .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExample(this.mappingContext, example)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Flux findAll(Example example, Sort sort) { return this.neo4jOperations - .toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER)) - .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + .toExecutableQuery(example.getProbeType(), + QueryFragmentsAndParameters.forExampleWithSort(this.mappingContext, example, sort, null, + PropertyFilter.NO_FILTER)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Mono count(Example example) { - Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.useWithReadingFragment(cypherGenerator::prepareMatchOf) - .returning(Cypher.count(asterisk())).build(); + Predicate predicate = Predicate.create(this.mappingContext, example); + Statement statement = predicate.useWithReadingFragment(this.cypherGenerator::prepareMatchOf) + .returning(Cypher.count(asterisk())) + .build(); return this.neo4jOperations.count(statement, predicate.getParameters()); } @@ -97,13 +104,15 @@ public final class SimpleReactiveQueryByExampleExecutor implements ReactiveQu } @Override - public > P findBy(Example example, Function, P> queryFunction) { + public > P findBy(Example example, + Function, P> queryFunction) { if (this.neo4jOperations instanceof ReactiveFluentFindOperation ops) { ReactiveFluentQuery fluentQuery = new ReactiveFluentQueryByExample<>(example, example.getProbeType(), - mappingContext, ops, this::count, this::exists); + this.mappingContext, ops, this::count, this::exists); return queryFunction.apply(fluentQuery); } throw new UnsupportedOperationException( "Fluent find by example not supported with standard Neo4jOperations, must support fluent queries too"); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 652f6d9f3..dba5b997f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.domain.Pageable; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.Neo4jOperations; @@ -44,15 +45,16 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Implementation of {@link RepositoryQuery} for query methods annotated with {@link Query @Query}. The flow to handle - * queries with SpEL parameters is as follows + * Implementation of {@link RepositoryQuery} for query methods annotated with + * {@link Query @Query}. The flow to handle queries with SpEL parameters is as follows *
    *
  1. Parse template as something that has SpEL-expressions in it
  2. *
  3. Replace the SpEL-expressions with Neo4j Statement template parameters
  4. - *
  5. The parameters passed here _and_ the values that might have been computed during SpEL-parsing
  6. + *
  7. The parameters passed here _and_ the values that might have been computed during + * SpEL-parsing
  8. *
- * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL expressions with real Neo4j - * parameters. + * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL + * expressions with real Neo4j parameters. * * @author Gerrit Meier * @author Michael J. Simons @@ -60,19 +62,16 @@ import org.springframework.util.StringUtils; */ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { - private final static String COMMENT_OR_WHITESPACE_GROUP = "(?:\\s|/\\\\*.*?\\\\*/|//.*?$)"; + private static final String COMMENT_OR_WHITESPACE_GROUP = "(?:\\s|/\\\\*.*?\\\\*/|//.*?$)"; static final Pattern SKIP_AND_LIMIT_WITH_PLACEHOLDER_PATTERN = Pattern - .compile("" - + "(?ims)" - + ".+SKIP" + COMMENT_OR_WHITESPACE_GROUP + "+" - + "\\$" + COMMENT_OR_WHITESPACE_GROUP + "*(?:(?-i)skip)" + COMMENT_OR_WHITESPACE_GROUP + "+" - + "LIMIT" + COMMENT_OR_WHITESPACE_GROUP + "+" - + "\\$" + COMMENT_OR_WHITESPACE_GROUP + "*(?:(?-i)limit)" - + ".*"); + .compile("" + "(?ims)" + ".+SKIP" + COMMENT_OR_WHITESPACE_GROUP + "+" + "\\$" + COMMENT_OR_WHITESPACE_GROUP + + "*(?:(?-i)skip)" + COMMENT_OR_WHITESPACE_GROUP + "+" + "LIMIT" + COMMENT_OR_WHITESPACE_GROUP + "+" + + "\\$" + COMMENT_OR_WHITESPACE_GROUP + "*(?:(?-i)limit)" + ".*"); /** - * Used to evaluate the expression found while parsing the cypher template of this query against the actual parameters - * with the help of the formal parameters during the building of the {@link PreparedQuery}. + * Used to evaluate the expression found while parsing the cypher template of this + * query against the actual parameters with the help of the formal parameters during + * the building of the {@link PreparedQuery}. */ private final ValueExpressionQueryRewriter.QueryExpressionEvaluator parsedQuery; @@ -83,22 +82,39 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { private final ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter queryRewriter; + private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, + Neo4jQueryType queryType, ProjectionFactory factory) { + + super(neo4jOperations, mappingContext, queryMethod, queryType, factory); + + cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, + queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER); + this.queryRewriter = ValueExpressionQueryRewriter.of(delegate, StringBasedNeo4jQuery::parameterNameSource, + StringBasedNeo4jQuery::replacementSource); + this.parsedQuery = this.queryRewriter.parse(cypherTemplate, queryMethod.getParameters()); + this.parsedCountQuery = queryMethod.getQueryAnnotation() + .map(Query::countQuery) + .map(q -> Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(q, mappingContext, + queryMethod.getEntityInformation(), delegate)) + .map(string -> this.queryRewriter.parse(string, queryMethod.getParameters())); + } + /** - * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The - * annotation is expected to have a value. - * - * @param neo4jOperations the Neo4j operations - * @param mappingContext a Neo4jMappingContext instance - * @param delegate a ValueExpressionDelegate instance - * @param queryMethod the query method - * @return A new instance of a String based Neo4j query. + * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with + * {@link Query @Query}. The annotation is expected to have a value. + * @param neo4jOperations the Neo4j operations + * @param mappingContext a Neo4jMappingContext instance + * @param delegate a ValueExpressionDelegate instance + * @param queryMethod the query method + * @param factory projection factory to use + * @return a new instance of a String based Neo4j query. */ static StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, - ProjectionFactory factory) { + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, ProjectionFactory factory) { Query queryAnnotation = queryMethod.getQueryAnnotation() - .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method")); + .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method")); boolean requiresCount = queryMethod.isPageQuery(); boolean supportsCount = queryMethod.isSliceQuery(); @@ -110,86 +126,93 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { throw new MappingException("Expected paging query method to have a count query"); } if (supportsCount) { - Neo4jQuerySupport.REPOSITORY_QUERY_LOG.debug(() -> String.format( - "You provided a string based query returning a slice for '%s.%s'. " - + "You might want to consider adding a count query if more slices than you expect are returned.", - queryMethod.getRepositoryName(), queryMethod.getName())); + Neo4jQuerySupport.REPOSITORY_QUERY_LOG + .debug(() -> String.format("You provided a string based query returning a slice for '%s.%s'. " + + "You might want to consider adding a count query if more slices than you expect are returned.", + queryMethod.getRepositoryName(), queryMethod.getName())); } } - String cypherTemplate = Optional.ofNullable(queryAnnotation.value()).filter(StringUtils::hasText) - .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not")); + String cypherTemplate = Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not")); if (requiresSkipAndLimit && !hasSkipAndLimitKeywordsAndPlaceholders(cypherTemplate)) { - Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> - String.format("The custom query %n%s%n" - + "for '%s.%s' is supposed to work with a page or slicing query but does not have the required " - + "parameter placeholders `$skip` and `$limit`.%n" - + "Be aware that those parameters are case sensitive and SDN uses the lower case variant.", - cypherTemplate, queryMethod.getRepositoryName(), queryMethod.getName())); + Neo4jQuerySupport.REPOSITORY_QUERY_LOG.warn(() -> String.format("The custom query %n%s%n" + + "for '%s.%s' is supposed to work with a page or slicing query but does not have the required " + + "parameter placeholders `$skip` and `$limit`.%n" + + "Be aware that those parameters are case sensitive and SDN uses the lower case variant.", + cypherTemplate, queryMethod.getRepositoryName(), queryMethod.getName())); } - return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, - cypherTemplate, Neo4jQueryType.fromDefinition(queryAnnotation), factory); + return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, cypherTemplate, + Neo4jQueryType.fromDefinition(queryAnnotation), factory); } /** * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. - * - * @param neo4jOperations the Neo4j operations - * @param mappingContext a Neo4jMappingContext instance - * @param delegate a ValueExpressionDelegate instance - * @param queryMethod the query method - * @param cypherTemplate The template to use. - * @return A new instance of a String based Neo4j query. + * @param neo4jOperations the Neo4j operations + * @param mappingContext a Neo4jMappingContext instance + * @param delegate a ValueExpressionDelegate instance + * @param queryMethod the query method + * @param cypherTemplate the template to use. + * @param factory projection factory to use + * @return a new instance of a String based Neo4j query. */ static StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, - String cypherTemplate, ProjectionFactory factory) { + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, + ProjectionFactory factory) { Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template"); - return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, - cypherTemplate, Neo4jQueryType.DEFAULT, factory); + return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, cypherTemplate, + Neo4jQueryType.DEFAULT, factory); } - private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, - String cypherTemplate, Neo4jQueryType queryType, ProjectionFactory factory) { + /** + * Generates new parameter names. + * @param index position of this parameter placeholder + * @param originalSpelExpression not used for configuring parameter names atm. + * @return a new parameter name for the given index. + */ + static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { + return "__SpEL__" + index; + } - super(neo4jOperations, mappingContext, queryMethod, queryType, factory); + /** + * Replaces parameter names. + * @param originalPrefix the prefix passed to the replacement source is either ':' or + * '?', so that isn't usable for Cypher templates and therefore ignored. + * @param parameterName name of the parameter + * @return the name of the parameter in its native Cypher form. + */ + static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { + return "$" + parameterName; + } - cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER); - this.queryRewriter = ValueExpressionQueryRewriter.of(delegate, - StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource); - this.parsedQuery = queryRewriter.parse(cypherTemplate, queryMethod.getParameters()); - this.parsedCountQuery = queryMethod.getQueryAnnotation() - .map(Query::countQuery) - .map(q -> Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(q, mappingContext, queryMethod.getEntityInformation(), delegate)) - .map(string -> queryRewriter.parse(string, queryMethod.getParameters())); + static boolean hasSkipAndLimitKeywordsAndPlaceholders(String queryTemplate) { + return SKIP_AND_LIMIT_WITH_PLACEHOLDER_PATTERN.matcher(queryTemplate).matches(); } @Override - protected PreparedQuery prepareQuery(Class returnedType, Collection includedProperties, - Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, + protected PreparedQuery prepareQuery(Class returnedType, + Collection includedProperties, Neo4jParameterAccessor parameterAccessor, + @Nullable Neo4jQueryType queryType, @Nullable Supplier> mappingFunction, - UnaryOperator limitModifier - ) { + UnaryOperator limitModifier) { Map boundParameters = bindParameters(parameterAccessor, true, limitModifier); QueryContext queryContext = new QueryContext( - queryMethod.getRepositoryName() + "." + queryMethod.getName(), - parsedQuery.getQueryString(), - boundParameters - ); + this.queryMethod.getRepositoryName() + "." + this.queryMethod.getName(), + this.parsedQuery.getQueryString(), boundParameters); logWarningsIfNecessary(queryContext, parameterAccessor); return PreparedQuery.queryFor(returnedType) - .withCypherQuery(queryContext.query) - .withParameters(boundParameters) - .usingMappingFunction(mappingFunction) - .build(); + .withCypherQuery(queryContext.query) + .withParameters(boundParameters) + .usingMappingFunction(mappingFunction) + .build(); } Map bindParameters(Neo4jParameterAccessor parameterAccessor, boolean includePageableParameter, @@ -199,7 +222,8 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { Map resolvedParameters = new HashMap<>(); // Values from the parameter accessor can only get converted after evaluation - for (Entry evaluatedParam : parsedQuery.evaluate(parameterAccessor.getValues()).entrySet()) { + for (Entry evaluatedParam : this.parsedQuery.evaluate(parameterAccessor.getValues()) + .entrySet()) { Object value = evaluatedParam.getValue(); if (!(evaluatedParam.getValue() instanceof LiteralReplacement)) { Neo4jQuerySupport.logParameterIfNull(evaluatedParam.getKey(), value); @@ -232,42 +256,19 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { @Override protected Optional> getCountQuery(Neo4jParameterAccessor parameterAccessor) { - return parsedCountQuery.map(ValueExpressionQueryRewriter.QueryExpressionEvaluator::getQueryString) - .map(countQuery -> { - Map boundParameters = bindParameters(parameterAccessor, false, UnaryOperator.identity()); - QueryContext queryContext = new QueryContext( - queryMethod.getRepositoryName() + "." + queryMethod.getName(), - countQuery, - boundParameters - ); + return this.parsedCountQuery.map(ValueExpressionQueryRewriter.QueryExpressionEvaluator::getQueryString) + .map(countQuery -> { + Map boundParameters = bindParameters(parameterAccessor, false, + UnaryOperator.identity()); + QueryContext queryContext = new QueryContext( + this.queryMethod.getRepositoryName() + "." + this.queryMethod.getName(), countQuery, + boundParameters); - return PreparedQuery.queryFor(Long.class) - .withCypherQuery(queryContext.query) - .withParameters(boundParameters) - .build(); - }); + return PreparedQuery.queryFor(Long.class) + .withCypherQuery(queryContext.query) + .withParameters(boundParameters) + .build(); + }); } - /** - * @param index position of this parameter placeholder - * @param originalSpelExpression Not used for configuring parameter names atm. - * @return A new parameter name for the given index. - */ - static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { - return "__SpEL__" + index; - } - - /** - * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for - * Cypher templates and therefore ignored. - * @param parameterName name of the parameter - * @return The name of the parameter in its native Cypher form. - */ - static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { - return "$" + parameterName; - } - - static boolean hasSkipAndLimitKeywordsAndPlaceholders(String queryTemplate) { - return SKIP_AND_LIMIT_WITH_PLACEHOLDER_PATTERN.matcher(queryTemplate).matches(); - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java index 47ba55d7f..17ea03191 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * A set of annotations for providing custom queries to repositories. */ diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslConditionExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslConditionExecutor.java index 5961b80d3..ec7d885e6 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslConditionExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslConditionExecutor.java @@ -21,16 +21,18 @@ import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.SortItem; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; /** - * An interface that can be added to any repository so that queries can be enriched by {@link Condition conditions} of the - * Cypher-DSL. This interface behaves the same as the {@link org.springframework.data.querydsl.QuerydslPredicateExecutor}. + * An interface that can be added to any repository so that queries can be enriched by + * {@link Condition conditions} of the Cypher-DSL. This interface behaves the same as the + * {@link org.springframework.data.querydsl.QuerydslPredicateExecutor}. * + * @param type of the domain * @author Michael J. Simons - * @param Type of the domain * @since 6.1 */ @API(status = API.Status.STABLE, since = "6.1") @@ -51,5 +53,5 @@ public interface CypherdslConditionExecutor { long count(Condition condition); boolean exists(Condition condition); -} +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslStatementExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslStatementExecutor.java index 182e9436e..279c96eb5 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslStatementExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/CypherdslStatementExecutor.java @@ -21,84 +21,91 @@ import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; /** - * An interface that can be added to any imperative repository so that the repository exposes several methods taking in - * a {@link Statement} from the Cypher-DSL, that allows for full customization of the queries executed in a programmatic - * way in contrast to provide custom queries declaratively via {@link org.springframework.data.neo4j.repository.query.Query @Query} - * annotations. - + * An interface that can be added to any imperative repository so that the repository + * exposes several methods taking in a {@link Statement} from the Cypher-DSL, that allows + * for full customization of the queries executed in a programmatic way in contrast to + * provide custom queries declaratively via + * {@link org.springframework.data.neo4j.repository.query.Query @Query} annotations. + * + * @param the domain type of the repository * @author Michael J. Simons - * @param The domain type of the repository - * @soundtrack Queen - Queen On Air * @since 6.1 */ @API(status = API.Status.STABLE, since = "6.1") public interface CypherdslStatementExecutor { /** - * Find one element of the domain as defined by the {@code statement}. The statement must return either no or exactly - * one mappable record. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @return An empty optional or an optional containing the single element + * Find one element of the domain as defined by the {@code statement}. The statement + * must return either no or exactly one mappable record. + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @return an empty optional or an optional containing the single element */ Optional findOne(Statement statement); /** - * Creates a custom projection of the repository type by a Cypher-DSL based statement. The statement must return either - * no or exactly one mappable record. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @param projectionClass The class of the projection type - * @param The type of the projection - * @return An empty optional or an optional containing the single, projected element + * Creates a custom projection of the repository type by a Cypher-DSL based statement. + * The statement must return either no or exactly one mappable record. + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @param projectionClass the class of the projection type + * @param the type of the projection + * @return an empty optional or an optional containing the single, projected element */ Optional findOne(Statement statement, Class projectionClass); /** * Find all elements of the domain as defined by the {@code statement}. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @return An iterable full of domain objects + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @return an iterable full of domain objects */ Collection findAll(Statement statement); /** * Creates a custom projection of the repository type by a Cypher-DSL based statement. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @param projectionClass The class of the projection type - * @param The type of the projection - * @return An iterable full of projections + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @param projectionClass the class of the projection type + * @param the type of the projection + * @return an iterable full of projections */ Collection findAll(Statement statement, Class projectionClass); /** * The pages here are built with a fragment of a {@link Statement}: An - * {@link OngoingReadingAndReturn ongoing reading with an attached return}. The next step is ordering the results, - * and that order will be derived from the {@code pageable}. The same applies for the values of skip and limit. - * - * @param statement The almost complete statement that actually matches and returns the nodes and relationships to be projected - * @param countQuery The statement that is executed to count the total number of matches for computing the correct number of pages - * @param pageable The definition of the page - * @return A page full of domain objects + * {@link OngoingReadingAndReturn ongoing reading with an attached return}. The next + * step is ordering the results, and that order will be derived from the + * {@code pageable}. The same applies for the values of skip and limit. + * @param statement the almost complete statement that actually matches and returns + * the nodes and relationships to be projected + * @param countQuery the statement that is executed to count the total number of + * matches for computing the correct number of pages + * @param pageable the definition of the page + * @return a page full of domain objects */ Page findAll(OngoingReadingAndReturn statement, Statement countQuery, Pageable pageable); /** * The pages here are built with a fragment of a {@link Statement}: An - * {@link OngoingReadingAndReturn ongoing reading with an attached return}. The next step is ordering the results, - * and that order will be derived from the {@code pageable}. The same applies for the values of skip and limit. - * - * @param statement The almost complete statement that actually matches and returns the nodes and relationships to be projected - * @param countQuery The statement that is executed to count the total number of matches for computing the correct number of pages - * @param pageable The definition of the page - * @param projectionClass The class of the projection type - * @param The type of the projection - * @return A page full of projections + * {@link OngoingReadingAndReturn ongoing reading with an attached return}. The next + * step is ordering the results, and that order will be derived from the + * {@code pageable}. The same applies for the values of skip and limit. + * @param statement the almost complete statement that actually matches and returns + * the nodes and relationships to be projected + * @param countQuery the statement that is executed to count the total number of + * matches for computing the correct number of pages + * @param pageable the definition of the page + * @param projectionClass the class of the projection type + * @param the type of the projection + * @return a page full of projections */ - Page findAll(OngoingReadingAndReturn statement, Statement countQuery, Pageable pageable, Class projectionClass); + Page findAll(OngoingReadingAndReturn statement, Statement countQuery, Pageable pageable, + Class projectionClass); + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java b/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java index 3a8338c7b..27767e3c4 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java @@ -21,8 +21,9 @@ import org.springframework.data.repository.core.support.PersistentEntityInformat /** * Default implementation of Neo4j specific entity information. * + * @param the entity type + * @param the ID type * @author Michael J. Simons - * @soundtrack Bear McCreary - Battlestar Galactica Season 1 * @since 6.0 */ final class DefaultNeo4jEntityInformation extends PersistentEntityInformation @@ -35,12 +36,9 @@ final class DefaultNeo4jEntityInformation extends PersistentEntityInforma this.entityMetaData = entityMetaData; } - /* - * (non-Javadoc) - * @see Neo4jEntityInformation#getEntityMetaData() - */ @Override public Neo4jPersistentEntity getEntityMetaData() { return this.entityMetaData; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java b/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java index 9fbf51e81..d513e7b8f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/EntityAndGraphPropertyAccessingMethodInterceptor.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.NotReadablePropertyException; @@ -35,26 +36,13 @@ import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** - * Basically a lenient property accessing method interceptor, first trying the entity property (or attribute), than - * a potentially renamed attribute via {@link Property}. + * Basically a lenient property accessing method interceptor, first trying the entity + * property (or attribute), than a potentially renamed attribute via {@link Property}. * * @author Michael J. Simons */ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodInterceptor { - static MethodInterceptorFactory createMethodInterceptorFactory(Neo4jMappingContext mappingContext) { - return new MethodInterceptorFactory() { - @Override - public MethodInterceptor createMethodInterceptor(Object source, Class targetType) { - return new EntityAndGraphPropertyAccessingMethodInterceptor(source, mappingContext); - } - - @Override public boolean supports(Object source, Class targetType) { - return true; - } - }; - } - private final BeanWrapper target; private EntityAndGraphPropertyAccessingMethodInterceptor(Object target, Neo4jMappingContext ctx) { @@ -63,9 +51,26 @@ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodIn this.target = new GraphPropertyAndDirectFieldAccessFallbackBeanWrapper(target, ctx); } + static MethodInterceptorFactory createMethodInterceptorFactory(Neo4jMappingContext mappingContext) { + return new MethodInterceptorFactory() { + @Override + public MethodInterceptor createMethodInterceptor(Object source, Class targetType) { + return new EntityAndGraphPropertyAccessingMethodInterceptor(source, mappingContext); + } + + @Override + public boolean supports(Object source, Class targetType) { + return true; + } + }; + } + + private static boolean isSetterMethod(Method method, PropertyDescriptor descriptor) { + return method.equals(descriptor.getWriteMethod()); + } + @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); @@ -80,26 +85,23 @@ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodIn } if (!isSetterMethod(method, descriptor)) { - return target.getPropertyValue(descriptor.getName()); + return this.target.getPropertyValue(descriptor.getName()); } if (invocation.getArguments().length != 1) { throw new IllegalStateException("Invoked setter method requires exactly one argument"); } - target.setPropertyValue(descriptor.getName(), invocation.getArguments()[0]); + this.target.setPropertyValue(descriptor.getName(), invocation.getArguments()[0]); return null; } - private static boolean isSetterMethod(Method method, PropertyDescriptor descriptor) { - return method.equals(descriptor.getWriteMethod()); - } - /** - * this version of the {@link DirectFieldAccessFallbackBeanWrapper} checks if there's an attribute on the entity - * annotated with {@link Property} mapping it to a different graph property when it fails to access the original - * attribute If so, that property is accessed. If not, the original exception is rethrown. - * This helps in projections such as described here + * this version of the {@link DirectFieldAccessFallbackBeanWrapper} checks if there's + * an attribute on the entity annotated with {@link Property} mapping it to a + * different graph property when it fails to access the original attribute If so, that + * property is accessed. If not, the original exception is rethrown. This helps in + * projections such as described here * https://stackoverflow.com/questions/68938823/sdn6-projection-interfaces-with-property-mapping * that could have been used as workaround prior to fixing 2371. */ @@ -113,28 +115,28 @@ final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodIn } @Override - @Nullable - public Object getPropertyValue(String propertyName) { + @Nullable public Object getPropertyValue(String propertyName) { try { return super.getPropertyValue(propertyName); - } catch (NotReadablePropertyException e) { - Neo4jPersistentEntity entity = ctx.getPersistentEntity(super.getRootClass()); + } + catch (NotReadablePropertyException ex) { + Neo4jPersistentEntity entity = this.ctx.getPersistentEntity(super.getRootClass()); AtomicReference value = new AtomicReference<>(); if (entity != null) { - PropertyHandlerSupport.of(entity).doWithProperties( - p -> { - if (p.findAnnotation(Property.class) != null && p.getPropertyName() - .equals(propertyName)) { - value.compareAndSet(null, p.getFieldName()); - } - }); + PropertyHandlerSupport.of(entity).doWithProperties(p -> { + if (p.findAnnotation(Property.class) != null && p.getPropertyName().equals(propertyName)) { + value.compareAndSet(null, p.getFieldName()); + } + }); if (value.get() != null) { return super.getPropertyValue(value.get()); } } - throw e; + throw ex; } } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java index a5914e9ac..d0c12e464 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java @@ -21,16 +21,17 @@ import org.springframework.data.repository.core.EntityInformation; /** * Neo4j specific contract for {@link EntityInformation entity informations}. * + * @param the type of the entity + * @param the type of the id * @author Michael J. Simons - * @param The type of the entity - * @param The type of the id - * @soundtrack Bear McCreary - Battlestar Galactica Season 1 * @since 6.0 */ public interface Neo4jEntityInformation extends EntityInformation { /** - * @return The full schema based description for the underlying entity. + * Returns the full schema based description for the underlying entity. + * @return the full schema based description for the underlying entity */ Neo4jPersistentEntity getEntityMetaData(); + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java index 2be48094e..01241c855 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java @@ -15,22 +15,23 @@ */ package org.springframework.data.neo4j.repository.support; -import static org.apiguardian.api.API.Status.INTERNAL; - import java.util.HashMap; import java.util.Map; import org.apiguardian.api.API; + import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.spel.spi.Function; import org.springframework.data.util.ReflectionUtils; +import static org.apiguardian.api.API.Status.INTERNAL; + /** - * This class registers the Neo4j SpEL Support it is registered by the appropriate repository factories as a root bean. + * This class registers the Neo4j SpEL Support it is registered by the appropriate + * repository factories as a root bean. * * @author Michael J. Simons - * @soundtrack Red Hot Chili Peppers - Californication * @since 6.0.2 */ @API(status = INTERNAL, since = "6.0.2") @@ -47,14 +48,15 @@ public final class Neo4jEvaluationContextExtension implements EvaluationContextE public Map getFunctions() { Map functions = new HashMap<>(); functions.put(Neo4jSpelSupport.FUNCTION_ORDER_BY, new Function(ReflectionUtils - .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_LITERAL, new Function(ReflectionUtils - .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_ANY_OF, new Function(ReflectionUtils - .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_ALL_OF, new Function(ReflectionUtils - .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class))); + .getRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class))); return functions; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index bd7ff31a5..c1b0cbf43 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.renderer.Configuration; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.neo4j.core.Neo4jOperations; @@ -66,7 +67,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override public Neo4jEntityInformation getEntityInformation(RepositoryMetadata metadata) { - Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + Neo4jPersistentEntity entity = this.mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); return new DefaultNeo4jEntityInformation<>(entity); } @@ -75,7 +76,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jEntityInformation entityInformation = getEntityInformation(metadata); Neo4jRepositoryFactorySupport.assertIdentifierType(metadata.getIdType(), entityInformation.getIdType()); - return getTargetRepositoryViaReflection(metadata, neo4jOperations, entityInformation); + return getTargetRepositoryViaReflection(metadata, this.neo4jOperations, entityInformation); } @Override @@ -83,17 +84,18 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { RepositoryFragments fragments = RepositoryFragments.empty(); - Object byExampleExecutor = instantiateClass(SimpleQueryByExampleExecutor.class, neo4jOperations, - mappingContext); + Object byExampleExecutor = instantiateClass(SimpleQueryByExampleExecutor.class, this.neo4jOperations, + this.mappingContext); fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); if (isQueryDslRepository) { - fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class)); + fragments = fragments + .append(createDSLPredicateExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class)); } if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { @@ -104,10 +106,12 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { return fragments; } - private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { + private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, + Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); + Object querydslFragment = instantiateClass(implementor, this.mappingContext, entityInformation, + this.neo4jOperations); return RepositoryFragment.implemented(querydslFragment); } @@ -115,7 +119,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); + Object querydslFragment = instantiateClass(implementor, entityInformation, this.neo4jOperations); return RepositoryFragment.implemented(querydslFragment); } @@ -128,15 +132,15 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { super.setBeanFactory(beanFactory); - this.cypherDSLConfiguration = beanFactory - .getBeanProvider(Configuration.class) - .getIfAvailable(Configuration::defaultConfig); + this.cypherDSLConfiguration = beanFactory.getBeanProvider(Configuration.class) + .getIfAvailable(Configuration::defaultConfig); } @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { - return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); + return Optional.of(new Neo4jQueryLookupStrategy(this.neo4jOperations, this.mappingContext, + valueExpressionDelegate, this.cypherDSLConfiguration)); } @Override @@ -144,9 +148,11 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { ProjectionFactory projectionFactory = super.getProjectionFactory(); if (projectionFactory instanceof SpelAwareProxyProjectionFactory) { - ((SpelAwareProxyProjectionFactory) projectionFactory).registerMethodInvokerFactory( - EntityAndGraphPropertyAccessingMethodInterceptor.createMethodInterceptorFactory(mappingContext)); + ((SpelAwareProxyProjectionFactory) projectionFactory) + .registerMethodInvokerFactory(EntityAndGraphPropertyAccessingMethodInterceptor + .createMethodInterceptorFactory(this.mappingContext)); } return projectionFactory; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 0eb9290b7..8e5309e6d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -19,6 +19,7 @@ import java.io.Serializable; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -27,14 +28,14 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; /** - * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of - * repository factories via Spring configuration. + * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} + * interface to allow easy setup of repository factories via Spring configuration. * + * @param the type of the repository + * @param type of the domain class to map + * @param identifier type in the domain class * @author Michael J. Simons * @author Gerrit Meier - * @param the type of the repository - * @param type of the domain class to map - * @param identifier type in the domain class * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -48,8 +49,8 @@ public final class Neo4jRepositoryFactoryBean, S, ID private Neo4jMappingContext neo4jMappingContext; /** - * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. - * + * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given + * repository interface. * @param repositoryInterface must not be {@literal null}. */ Neo4jRepositoryFactoryBean(Class repositoryInterface) { @@ -69,8 +70,10 @@ public final class Neo4jRepositoryFactoryBean, S, ID @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { if (this.neo4jOperations == null || this.neo4jMappingContext == null) { - throw new IllegalStateException("Repository factory bean has not been configured properly, both Neo4j operations and mapping context are required"); + throw new IllegalStateException( + "Repository factory bean has not been configured properly, both Neo4j operations and mapping context are required"); } - return new Neo4jRepositoryFactory(neo4jOperations, neo4jMappingContext); + return new Neo4jRepositoryFactory(this.neo4jOperations, this.neo4jMappingContext); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java index 260200e6f..e4412f086 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java @@ -23,8 +23,8 @@ import java.util.stream.Collectors; import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.BeanManager; - import org.apiguardian.api.API; + import org.springframework.data.neo4j.config.Neo4jCdiExtension; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -32,11 +32,11 @@ import org.springframework.data.repository.cdi.CdiRepositoryBean; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; /** - * The CDI pendant to the {@link Neo4jRepositoryFactoryBean}. It creates instances of {@link Neo4jRepositoryFactory}. + * The CDI pendant to the {@link Neo4jRepositoryFactoryBean}. It creates instances of + * {@link Neo4jRepositoryFactory}. * + * @param the type of the repository being created * @author Michael J. Simons - * @param The type of the repository being created - * @soundtrack Various - TRON Legacy R3conf1gur3d * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -44,8 +44,8 @@ public final class Neo4jRepositoryFactoryCdiBean extends CdiRepositoryBean private final BeanManager beanManager; - public Neo4jRepositoryFactoryCdiBean(Set qualifiers, Class repositoryType, - BeanManager beanManager, CustomRepositoryImplementationDetector detector) { + public Neo4jRepositoryFactoryCdiBean(Set qualifiers, Class repositoryType, BeanManager beanManager, + CustomRepositoryImplementationDetector detector) { super(qualifiers, repositoryType, beanManager, Optional.of(detector)); this.beanManager = beanManager; @@ -62,15 +62,17 @@ public final class Neo4jRepositoryFactoryCdiBean extends CdiRepositoryBean private RT getReference(Class clazz, CreationalContext creationalContext) { - Set> beans = beanManager.getBeans(clazz, Neo4jCdiExtension.ANY_BEAN); + Set> beans = this.beanManager.getBeans(clazz, Neo4jCdiExtension.ANY_BEAN); if (beans.size() > 1) { beans = beans.stream() - .filter(b -> b.getQualifiers().contains(Neo4jCdiExtension.DEFAULT_BEAN)) - .collect(Collectors.toSet()); + .filter(b -> b.getQualifiers().contains(Neo4jCdiExtension.DEFAULT_BEAN)) + .collect(Collectors.toSet()); } @SuppressWarnings("unchecked") - RT beanReference = (RT) beanManager.getReference(beanManager.resolve(beans), clazz, creationalContext); + RT beanReference = (RT) this.beanManager.getReference(this.beanManager.resolve(beans), clazz, + creationalContext); return beanReference; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupport.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupport.java index 9655b2688..3993354ed 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupport.java @@ -22,6 +22,9 @@ package org.springframework.data.neo4j.repository.support; */ final class Neo4jRepositoryFactorySupport { + private Neo4jRepositoryFactorySupport() { + } + static void assertIdentifierType(Class repositoryIdType, Class entityIdType) { if (repositoryIdType.equals(entityIdType) || isCompatibleType(repositoryIdType, entityIdType)) { @@ -49,6 +52,4 @@ final class Neo4jRepositoryFactorySupport { || repositoryIdType.equals(int.class) && entityIdType.equals(Integer.class); } - private Neo4jRepositoryFactorySupport() { - } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java index 29facb719..509a9a414 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java @@ -18,18 +18,19 @@ package org.springframework.data.neo4j.repository.support; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.SortItem; -import org.springframework.data.domain.Sort; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.data.domain.Sort; + /** - * An interface that can be added to any repository so that queries can be enriched by {@link Condition conditions} of the - * Cypher-DSL. This interface behaves the same as the {@link org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor}. + * An interface that can be added to any repository so that queries can be enriched by + * {@link Condition conditions} of the Cypher-DSL. This interface behaves the same as the + * {@link org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor}. * + * @param the type of the domain * @author Niklas Krieger * @author Michael J. Simons - * @param Type of the domain * @since 6.3.3 */ @API(status = API.Status.STABLE, since = "6.3.3") @@ -48,5 +49,5 @@ public interface ReactiveCypherdslConditionExecutor { Mono count(Condition condition); Mono exists(Condition condition); -} +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslStatementExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslStatementExecutor.java index b0f7f7247..e9c944be4 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslStatementExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslStatementExecutor.java @@ -15,61 +15,61 @@ */ package org.springframework.data.neo4j.repository.support; +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Statement; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.apiguardian.api.API; -import org.neo4j.cypherdsl.core.Statement; - /** - * An interface that can be added to any reactive repository so that the repository exposes several methods taking in - * a {@link Statement} from the Cypher-DSL, that allows for full customization of the queries executed in a programmatic - * way in contrast to provide custom queries declaratively via {@link org.springframework.data.neo4j.repository.query.Query @Query} - * annotations. + * An interface that can be added to any reactive repository so that the repository + * exposes several methods taking in a {@link Statement} from the Cypher-DSL, that allows + * for full customization of the queries executed in a programmatic way in contrast to + * provide custom queries declaratively via + * {@link org.springframework.data.neo4j.repository.query.Query @Query} annotations. * + * @param the domain type of the repository * @author Michael J. Simons - * @param The domain type of the repository - * @soundtrack Queen - Queen On Air * @since 6.1 */ @API(status = API.Status.STABLE, since = "6.1") public interface ReactiveCypherdslStatementExecutor { /** - * Find one element of the domain as defined by the {@code statement}. The statement must return either no or exactly - * one mappable record. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @return An empty Mono or a Mono containing the single element + * Find one element of the domain as defined by the {@code statement}. The statement + * must return either no or exactly one mappable record. + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @return an empty Mono or a Mono containing the single element */ Mono findOne(Statement statement); /** - * Creates a custom projection of the repository type by a Cypher-DSL based statement. The statement must return either - * no or exactly one mappable record. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @param projectionClass The class of the projection type - * @param The type of the projection - * @return An empty Mono or a Mono containing the single, projected element + * Creates a custom projection of the repository type by a Cypher-DSL based statement. + * The statement must return either no or exactly one mappable record. + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @param projectionClass the class of the projection type + * @param the type of the projection + * @return an empty Mono or a Mono containing the single, projected element */ Mono findOne(Statement statement, Class projectionClass); /** * Find all elements of the domain as defined by the {@code statement}. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @return A publisher full of domain objects + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @return a publisher full of domain objects */ Flux findAll(Statement statement); /** * Creates a custom projection of the repository type by a Cypher-DSL based statement. - * - * @param statement A full Cypher statement, matching and returning all required nodes, relationships and properties - * @param projectionClass The class of the projection type - * @param The type of the projection - * @return A publisher full of projections + * @param statement a full Cypher statement, matching and returning all required + * nodes, relationships and properties + * @param projectionClass the class of the projection type + * @param the type of the projection + * @return a publisher full of projections */ Flux findAll(Statement statement, Class projectionClass); + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index 07430e456..6717fd2d8 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.jspecify.annotations.Nullable; import org.neo4j.cypherdsl.core.renderer.Configuration; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; @@ -26,10 +27,10 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl; import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor; import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor; -import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QuerydslUtils; @@ -68,7 +69,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp @Override public Neo4jEntityInformation getEntityInformation(RepositoryMetadata metadata) { - Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + Neo4jPersistentEntity entity = this.mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); return new DefaultNeo4jEntityInformation<>(entity); } @@ -77,7 +78,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp Neo4jEntityInformation entityInformation = getEntityInformation(metadata); Neo4jRepositoryFactorySupport.assertIdentifierType(metadata.getIdType(), entityInformation.getIdType()); - return getTargetRepositoryViaReflection(metadata, neo4jOperations, entityInformation); + return getTargetRepositoryViaReflection(metadata, this.neo4jOperations, entityInformation); } @Override @@ -86,30 +87,34 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp RepositoryFragments fragments = RepositoryFragments.empty(); SimpleReactiveQueryByExampleExecutor byExampleExecutor = instantiateClass( - SimpleReactiveQueryByExampleExecutor.class, neo4jOperations, mappingContext); + SimpleReactiveQueryByExampleExecutor.class, this.neo4jOperations, this.mappingContext); fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT - && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); if (isQueryDslRepository) { - fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); + fragments = fragments + .append(createDSLPredicateExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); } if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { - fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveCypherdslConditionExecutorImpl.class)); + fragments = fragments + .append(createDSLExecutorFragment(metadata, ReactiveCypherdslConditionExecutorImpl.class)); } return fragments; } - private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { + private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, + Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); + Object querydslFragment = instantiateClass(implementor, this.mappingContext, entityInformation, + this.neo4jOperations); return RepositoryFragment.implemented(querydslFragment); } @@ -117,7 +122,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); + Object querydslFragment = instantiateClass(implementor, entityInformation, this.neo4jOperations); return RepositoryFragment.implemented(querydslFragment); } @@ -127,11 +132,11 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp return SimpleReactiveNeo4jRepository.class; } - - @Override protected Optional getQueryLookupStrategy(@Nullable Key key, + @Override + protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { - return Optional - .of(new ReactiveNeo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); + return Optional.of(new ReactiveNeo4jQueryLookupStrategy(this.neo4jOperations, this.mappingContext, + valueExpressionDelegate, this.cypherDSLConfiguration)); } @Override @@ -147,9 +152,8 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp }); } - this.cypherDSLConfiguration = beanFactory - .getBeanProvider(Configuration.class) - .getIfAvailable(Configuration::defaultConfig); + this.cypherDSLConfiguration = beanFactory.getBeanProvider(Configuration.class) + .getIfAvailable(Configuration::defaultConfig); } @Override @@ -157,9 +161,11 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp ProjectionFactory projectionFactory = super.getProjectionFactory(); if (projectionFactory instanceof SpelAwareProxyProjectionFactory) { - ((SpelAwareProxyProjectionFactory) projectionFactory).registerMethodInvokerFactory( - EntityAndGraphPropertyAccessingMethodInterceptor.createMethodInterceptorFactory(mappingContext)); + ((SpelAwareProxyProjectionFactory) projectionFactory) + .registerMethodInvokerFactory(EntityAndGraphPropertyAccessingMethodInterceptor + .createMethodInterceptorFactory(this.mappingContext)); } return projectionFactory; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 30d0f21ce..efd058c83 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -19,6 +19,7 @@ import java.io.Serializable; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -27,14 +28,14 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; /** - * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of - * repository factories via Spring configuration. + * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} + * interface to allow easy setup of repository factories via Spring configuration. * - * @author Gerrit Meier - * @author Michael J. Simons * @param the type of the repository * @param type of the domain class to map * @param identifier type in the domain class + * @author Gerrit Meier + * @author Michael J. Simons * @since 6.0 */ @API(status = API.Status.INTERNAL, since = "6.0") @@ -48,8 +49,8 @@ public final class ReactiveNeo4jRepositoryFactoryBean repositoryInterface) { @@ -69,8 +70,10 @@ public final class ReactiveNeo4jRepositoryFactoryBean - * The interceptor uses all {@link PersistenceExceptionTranslator persistence exception translators} it finds in the - * context through a {@link ChainedPersistenceExceptionTranslator}. Translations are eventually done with - * {@link DataAccessUtils#translateIfNecessary(RuntimeException, PersistenceExceptionTranslator)} which returns the - * original exception in case translation is not possible (the translator returned null). + * The interceptor uses all {@link PersistenceExceptionTranslator persistence exception + * translators} it finds in the context through a + * {@link ChainedPersistenceExceptionTranslator}. Translations are eventually done with + * {@link DataAccessUtils#translateIfNecessary(RuntimeException, PersistenceExceptionTranslator)} + * which returns the original exception in case translation is not possible (the + * translator returned null). * * @author Michael J. Simons - * @soundtrack Fatoni - Andorra * @since 6.0 */ final class ReactivePersistenceExceptionTranslationInterceptor implements MethodInterceptor { @@ -55,10 +57,10 @@ final class ReactivePersistenceExceptionTranslationInterceptor implements Method private volatile PersistenceExceptionTranslator persistenceExceptionTranslator; /** - * Create a new PersistenceExceptionTranslationInterceptor, autodetecting PersistenceExceptionTranslators in the given - * BeanFactory. - * - * @param beanFactory the ListableBeanFactory to obtaining all PersistenceExceptionTranslators from + * Create a new PersistenceExceptionTranslationInterceptor, autodetecting + * PersistenceExceptionTranslators in the given BeanFactory. + * @param beanFactory the ListableBeanFactory to obtaining all + * PersistenceExceptionTranslators from */ @SuppressWarnings("NullAway") ReactivePersistenceExceptionTranslationInterceptor(ListableBeanFactory beanFactory) { @@ -67,21 +69,23 @@ final class ReactivePersistenceExceptionTranslationInterceptor implements Method } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + @Nullable public Object invoke(MethodInvocation mi) throws Throwable { // Invoke the method potentially returning a reactive type Object m = mi.proceed(); PersistenceExceptionTranslator translator = getPersistenceExceptionTranslator(); - // Add the translation. Nothing will happen if no-one subscribe the reactive result. - Function errorMappingFunction = t -> t instanceof DataAccessException ? t + // Add the translation. Nothing will happen if no-one subscribe the reactive + // result. + Function errorMappingFunction = t -> (t instanceof DataAccessException) ? t : DataAccessUtils.translateIfNecessary(t, translator); if (m instanceof Mono) { return ((Mono) m).onErrorMap(RuntimeException.class, errorMappingFunction); - } else if (m instanceof Flux) { + } + else if (m instanceof Flux) { return ((Flux) m).onErrorMap(RuntimeException.class, errorMappingFunction); - } else { + } + else { return m; } } @@ -103,17 +107,17 @@ final class ReactivePersistenceExceptionTranslationInterceptor implements Method /** * Detect all PersistenceExceptionTranslators in the given BeanFactory. - * - * @return a chained PersistenceExceptionTranslator, combining all PersistenceExceptionTranslators found in the - * factory + * @return a chained PersistenceExceptionTranslator, combining all + * PersistenceExceptionTranslators found in the factory * @see ChainedPersistenceExceptionTranslator */ private PersistenceExceptionTranslator detectPersistenceExceptionTranslators() { // Find all translators, being careful not to activate FactoryBeans. - Map pets = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, - PersistenceExceptionTranslator.class, false, false); + Map pets = BeanFactoryUtils + .beansOfTypeIncludingAncestors(this.beanFactory, PersistenceExceptionTranslator.class, false, false); ChainedPersistenceExceptionTranslator cpet = new ChainedPersistenceExceptionTranslator(); pets.values().forEach(cpet::addDelegate); return cpet; } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java index 0c169cb09..9ade9cb6b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactivePersistenceExceptionTranslationPostProcessor.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.repository.support; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.io.Serial; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -25,6 +22,9 @@ import java.util.Objects; import org.aopalliance.aop.Advice; import org.apiguardian.api.API; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; @@ -39,20 +39,21 @@ import org.springframework.stereotype.Repository; import org.springframework.util.Assert; /** - * Bean post-processor that automatically applies persistence exception translation to all methods returning either - * {@link reactor.core.publisher.Mono} or {@link reactor.core.publisher.Flux} of any bean marked with - * Spring's @{@link Repository Repository} annotation, adding a corresponding - * {@link AbstractPointcutAdvisor} to the exposed proxy (either an existing AOP proxy or a newly - * generated proxy that implements all of the target's interfaces). + * Bean post-processor that automatically applies persistence exception translation to all + * methods returning either {@link reactor.core.publisher.Mono} or + * {@link reactor.core.publisher.Flux} of any bean marked with Spring's @{@link Repository + * Repository} annotation, adding a corresponding {@link AbstractPointcutAdvisor} to the + * exposed proxy (either an existing AOP proxy or a newly generated proxy that implements + * all of the target's interfaces). *

- * That proxy will modify the reactive types by the matched method and inject an exception translation into the reactive - * flow. + * That proxy will modify the reactive types by the matched method and inject an exception + * translation into the reactive flow. *

- * This class can be declared as a standard bean if you run a lot of custom repositories in which you use either the - * {@link ReactiveNeo4jTemplate} or the {@link ReactiveNeo4jClient}. + * This class can be declared as a standard bean if you run a lot of custom repositories + * in which you use either the {@link ReactiveNeo4jTemplate} or the + * {@link ReactiveNeo4jClient}. * * @author Michael J. Simons - * @soundtrack Fatoni - Andorra * @since 6.0 */ @API(status = API.Status.STABLE, since = "6.0") @@ -61,7 +62,8 @@ public final class ReactivePersistenceExceptionTranslationPostProcessor @Serial private static final long serialVersionUID = -8597336297033105680L; - private transient final Class repositoryAnnotationType; + + private final transient Class repositoryAnnotationType; public ReactivePersistenceExceptionTranslationPostProcessor() { @@ -87,21 +89,23 @@ public final class ReactivePersistenceExceptionTranslationPostProcessor } /** - * Spring AOP exception translation aspect for use at Repository or DAO layer level. Translates native persistence - * exceptions into Spring's DataAccessException hierarchy, based on a given PersistenceExceptionTranslator. + * Spring AOP exception translation aspect for use at Repository or DAO layer level. + * Translates native persistence exceptions into Spring's DataAccessException + * hierarchy, based on a given PersistenceExceptionTranslator. */ static final class ReactivePersistenceExceptionTranslationAdvisor extends AbstractPointcutAdvisor { @Serial private static final long serialVersionUID = 849460320459940956L; - private transient final ReactivePersistenceExceptionTranslationInterceptor advice; - private transient final AnnotationMatchingPointcut pointcut; + private final transient ReactivePersistenceExceptionTranslationInterceptor advice; + + private final transient AnnotationMatchingPointcut pointcut; /** * Create a new PersistenceExceptionTranslationAdvisor. - * - * @param beanFactory the ListableBeanFactory to obtaining all PersistenceExceptionTranslators from + * @param beanFactory the ListableBeanFactory to obtaining all + * PersistenceExceptionTranslators from * @param repositoryAnnotationType the annotation type to check for */ ReactivePersistenceExceptionTranslationAdvisor(ListableBeanFactory beanFactory, @@ -132,5 +136,7 @@ public final class ReactivePersistenceExceptionTranslationPostProcessor public Pointcut getPointcut() { return this.pointcut; } + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 0f12bb30d..d73d50863 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.apiguardian.api.API; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -39,12 +40,12 @@ import org.springframework.transaction.annotation.Transactional; /** * Repository base implementation for Neo4j. * + * @param the type of the domain class managed by this repository + * @param the type of the unique identifier of the domain class * @author Gerrit Meier * @author Michael J. Simons * @author Ján Šúr * @author Jens Schauder - * @param the type of the domain class managed by this repository - * @param the type of the unique identifier of the domain class * @since 6.0 */ @Repository @@ -68,13 +69,13 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Override public Optional findById(ID id) { - return neo4jOperations.findById(id, this.entityInformation.getJavaType()); + return this.neo4jOperations.findById(id, this.entityInformation.getJavaType()); } @Override public List findAllById(Iterable ids) { - return neo4jOperations.findAllById(ids, this.entityInformation.getJavaType()); + return this.neo4jOperations.findAllById(ids, this.entityInformation.getJavaType()); } @Override @@ -86,16 +87,18 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Override public List findAll(Sort sort) { - return this.neo4jOperations.toExecutableQuery(entityInformation.getJavaType(), - QueryFragmentsAndParameters.forPageableAndSort(entityMetaData, null, sort)) - .getResults(); + return this.neo4jOperations + .toExecutableQuery(this.entityInformation.getJavaType(), + QueryFragmentsAndParameters.forPageableAndSort(this.entityMetaData, null, sort)) + .getResults(); } @Override public Page findAll(Pageable pageable) { - List allResult = this.neo4jOperations.toExecutableQuery(entityInformation.getJavaType(), - QueryFragmentsAndParameters.forPageableAndSort(entityMetaData, pageable, null)) - .getResults(); + List allResult = this.neo4jOperations + .toExecutableQuery(this.entityInformation.getJavaType(), + QueryFragmentsAndParameters.forPageableAndSort(this.entityMetaData, pageable, null)) + .getResults(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); @@ -104,7 +107,7 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Override public long count() { - return neo4jOperations.count(this.entityInformation.getJavaType()); + return this.neo4jOperations.count(this.entityInformation.getJavaType()); } @Override @@ -138,12 +141,15 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Transactional public void delete(T entity) { - ID id = Objects.requireNonNull(this.entityInformation.getId(entity), "Cannot delete individual nodes without an id"); - if (entityMetaData.hasVersionProperty()) { - Neo4jPersistentProperty versionProperty = entityMetaData.getRequiredVersionProperty(); - Object versionValue = entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); - this.neo4jOperations.deleteByIdWithVersion(id, this.entityInformation.getJavaType(), versionProperty, versionValue); - } else { + ID id = Objects.requireNonNull(this.entityInformation.getId(entity), + "Cannot delete individual nodes without an id"); + if (this.entityMetaData.hasVersionProperty()) { + Neo4jPersistentProperty versionProperty = this.entityMetaData.getRequiredVersionProperty(); + Object versionValue = this.entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); + this.neo4jOperations.deleteByIdWithVersion(id, this.entityInformation.getJavaType(), versionProperty, + versionValue); + } + else { this.deleteById(id); } } @@ -159,8 +165,9 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< @Transactional public void deleteAll(Iterable entities) { - List ids = StreamSupport.stream(entities.spliterator(), false).map(this.entityInformation::getId) - .collect(Collectors.toList()); + List ids = StreamSupport.stream(entities.spliterator(), false) + .map(this.entityInformation::getId) + .collect(Collectors.toList()); this.neo4jOperations.deleteAllById(ids, this.entityInformation.getJavaType()); } @@ -171,4 +178,5 @@ public class SimpleNeo4jRepository implements PagingAndSortingRepository< this.neo4jOperations.deleteAll(this.entityInformation.getJavaType()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index acc884446..8a2787c2e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.repository.support; -import org.springframework.data.repository.reactive.ReactiveCrudRepository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -26,12 +22,15 @@ import java.util.stream.StreamSupport; import org.apiguardian.api.API; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -40,18 +39,18 @@ import org.springframework.util.Assert; /** * Repository base implementation for Neo4j. * + * @param the type of the domain class managed by this repository + * @param the type of the unique identifier of the domain class * @author Gerrit Meier * @author Michael J. Simons * @author Jens Schauder - * @param the type of the domain class managed by this repository - * @param the type of the unique identifier of the domain class * @since 6.0 */ @Repository @Transactional(readOnly = true) @API(status = API.Status.STABLE, since = "6.0") -public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository, - ReactiveCrudRepository { +public class SimpleReactiveNeo4jRepository + implements ReactiveSortingRepository, ReactiveCrudRepository { private final ReactiveNeo4jOperations neo4jOperations; @@ -70,7 +69,7 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo @Override public Mono findById(ID id) { - return neo4jOperations.findById(id, this.entityInformation.getJavaType()); + return this.neo4jOperations.findById(id, this.entityInformation.getJavaType()); } @Override @@ -97,9 +96,10 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo @Override public Flux findAll(Sort sort) { - return this.neo4jOperations.toExecutableQuery(entityInformation.getJavaType(), - QueryFragmentsAndParameters.forPageableAndSort(entityMetaData, null, sort)) - .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + return this.neo4jOperations + .toExecutableQuery(this.entityInformation.getJavaType(), + QueryFragmentsAndParameters.forPageableAndSort(this.entityMetaData, null, sort)) + .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override @@ -139,10 +139,6 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo return Flux.from(entityStream).concatMap(this::save); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(java.lang.Object) - */ @Override @Transactional public Mono deleteById(ID id) { @@ -150,10 +146,6 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo return this.neo4jOperations.deleteById(id, this.entityInformation.getJavaType()); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(org.reactivestreams.Publisher) - */ @Override @Transactional public Mono deleteById(Publisher idPublisher) { @@ -162,29 +154,24 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo return Mono.from(idPublisher).flatMap(this::deleteById); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#delete(java.lang.Object) - */ @Override @Transactional public Mono delete(T entity) { Objects.requireNonNull(entity, "The given entity must not be null"); - ID id = Objects.requireNonNull(this.entityInformation.getId(entity), "Cannot delete individual nodes without an id"); - if (entityMetaData.hasVersionProperty()) { - Neo4jPersistentProperty versionProperty = entityMetaData.getRequiredVersionProperty(); - Object versionValue = entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); - return this.neo4jOperations.deleteByIdWithVersion(id, this.entityInformation.getJavaType(), versionProperty, versionValue); - } else { + ID id = Objects.requireNonNull(this.entityInformation.getId(entity), + "Cannot delete individual nodes without an id"); + if (this.entityMetaData.hasVersionProperty()) { + Neo4jPersistentProperty versionProperty = this.entityMetaData.getRequiredVersionProperty(); + Object versionValue = this.entityMetaData.getPropertyAccessor(entity).getProperty(versionProperty); + return this.neo4jOperations.deleteByIdWithVersion(id, this.entityInformation.getJavaType(), versionProperty, + versionValue); + } + else { return this.deleteById(id); } } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAllById(java.lang.Iterable) - */ @Override @Transactional public Mono deleteAllById(Iterable ids) { @@ -194,25 +181,18 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo return this.neo4jOperations.deleteAllById(ids, this.entityInformation.getJavaType()); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(java.lang.Iterable) - */ @Override @Transactional public Mono deleteAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null"); - List ids = StreamSupport.stream(entities.spliterator(), false).map(this.entityInformation::getId) - .collect(Collectors.toList()); + List ids = StreamSupport.stream(entities.spliterator(), false) + .map(this.entityInformation::getId) + .collect(Collectors.toList()); return this.neo4jOperations.deleteAllById(ids, this.entityInformation.getJavaType()); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(org.reactivestreams.Publisher) - */ @Override @Transactional public Mono deleteAll(Publisher entitiesPublisher) { @@ -221,14 +201,11 @@ public class SimpleReactiveNeo4jRepository implements ReactiveSortingRepo return Flux.from(entitiesPublisher).concatMap(this::delete).then(); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll() - */ @Override @Transactional public Mono deleteAll() { return this.neo4jOperations.deleteAll(this.entityInformation.getJavaType()); } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java b/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java index a818be1ec..5730b3160 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/package-info.java @@ -1,8 +1,22 @@ +/* + * Copyright 2011-2025 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. + */ /** - * - This package provides a couple of public support classes for building custom imperative and reactive Spring Data Neo4j - repository base classes. The support classes are the same classes used by SDN itself. - * + * This package provides a couple of public support classes for + * building custom imperative and reactive Spring Data Neo4j repository base classes. The + * support classes are the same classes used by SDN itself. */ @NullMarked package org.springframework.data.neo4j.repository.support; diff --git a/src/main/java/org/springframework/data/neo4j/types/AbstractPoint.java b/src/main/java/org/springframework/data/neo4j/types/AbstractPoint.java index cc0a9fa54..aa8aae3ea 100644 --- a/src/main/java/org/springframework/data/neo4j/types/AbstractPoint.java +++ b/src/main/java/org/springframework/data/neo4j/types/AbstractPoint.java @@ -35,7 +35,7 @@ abstract non-sealed class AbstractPoint implements Neo4jPoint { @Override public final Integer getSrid() { - return srid; + return this.srid; } @Override @@ -47,11 +47,12 @@ abstract non-sealed class AbstractPoint implements Neo4jPoint { return false; } AbstractPoint that = (AbstractPoint) o; - return Objects.equals(coordinate, that.coordinate) && Objects.equals(srid, that.srid); + return Objects.equals(this.coordinate, that.coordinate) && Objects.equals(this.srid, that.srid); } @Override public int hashCode() { - return Objects.hash(coordinate, srid); + return Objects.hash(this.coordinate, this.srid); } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint2d.java b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint2d.java index 830766dac..5668d18e7 100644 --- a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint2d.java +++ b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint2d.java @@ -18,6 +18,8 @@ package org.springframework.data.neo4j.types; import org.apiguardian.api.API; /** + * A concrete, 2-dimensional cartesian point. + * * @author Michael J. Simons * @since 6.0 */ @@ -35,15 +37,16 @@ public final class CartesianPoint2d extends AbstractPoint { } public double getX() { - return coordinate.getX(); + return this.coordinate.getX(); } public double getY() { - return coordinate.getY(); + return this.coordinate.getY(); } @Override public String toString() { return "CartesianPoint2d{" + "x=" + getX() + ", y=" + getY() + '}'; } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java index df28351ad..42246a2ab 100644 --- a/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java +++ b/src/main/java/org/springframework/data/neo4j/types/CartesianPoint3d.java @@ -20,6 +20,8 @@ import java.util.Objects; import org.apiguardian.api.API; /** + * A concrete, 3-dimensional cartesian point. + * * @author Michael J. Simons * @since 6.0 */ @@ -37,19 +39,21 @@ public final class CartesianPoint3d extends AbstractPoint { } public double getX() { - return coordinate.getX(); + return this.coordinate.getX(); } public double getY() { - return coordinate.getY(); + return this.coordinate.getY(); } public Double getZ() { - return Objects.requireNonNull(coordinate.getZ(), "The underlying coordinate does not have a z-value (height)"); + return Objects.requireNonNull(this.coordinate.getZ(), + "The underlying coordinate does not have a z-value (height)"); } @Override public String toString() { return "CartesianPoint3d{" + "x=" + getX() + ", y=" + getY() + ", z=" + getZ() + '}'; } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/Coordinate.java b/src/main/java/org/springframework/data/neo4j/types/Coordinate.java index 56dadb9f6..9bfe62e72 100644 --- a/src/main/java/org/springframework/data/neo4j/types/Coordinate.java +++ b/src/main/java/org/springframework/data/neo4j/types/Coordinate.java @@ -19,11 +19,14 @@ import java.util.Objects; import org.jspecify.annotations.Nullable; - /** + * A generic coordinate type with x, y and z values having an arbitrary meaning. + * * @author Michael J. Simons + * @since 6.0.0 */ public final class Coordinate { + private final double x; private final double y; @@ -42,16 +45,15 @@ public final class Coordinate { } double getX() { - return x; + return this.x; } double getY() { - return y; + return this.y; } - @Nullable - Double getZ() { - return z; + @Nullable Double getZ() { + return this.z; } @Override @@ -63,11 +65,13 @@ public final class Coordinate { return false; } Coordinate that = (Coordinate) o; - return Double.compare(that.x, x) == 0 && Double.compare(that.y, y) == 0 && Objects.equals(z, that.z); + return Double.compare(that.x, this.x) == 0 && Double.compare(that.y, this.y) == 0 + && Objects.equals(this.z, that.z); } @Override public int hashCode() { - return Objects.hash(x, y, z); + return Objects.hash(this.x, this.y, this.z); } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint2d.java b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint2d.java index b8b564afd..be778dfd4 100644 --- a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint2d.java +++ b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint2d.java @@ -18,6 +18,8 @@ package org.springframework.data.neo4j.types; import org.apiguardian.api.API; /** + * A concrete, 2-dimensional geographic point with a specific coordinate system. + * * @author Michael J. Simons * @since 6.0 */ @@ -33,16 +35,17 @@ public final class GeographicPoint2d extends AbstractPoint { } public double getLongitude() { - return coordinate.getX(); + return this.coordinate.getX(); } public double getLatitude() { - return coordinate.getY(); + return this.coordinate.getY(); } @Override public String toString() { - return "GeographicPoint2d{" + "longitude=" + getLongitude() + ", latitude=" + getLatitude() + ", srid=" + getSrid() - + '}'; + return "GeographicPoint2d{" + "longitude=" + getLongitude() + ", latitude=" + getLatitude() + ", srid=" + + getSrid() + '}'; } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java index 419419cff..9fe87b298 100644 --- a/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java +++ b/src/main/java/org/springframework/data/neo4j/types/GeographicPoint3d.java @@ -20,6 +20,8 @@ import java.util.Objects; import org.apiguardian.api.API; /** + * A concrete, 3-dimensional geographic point with a specific coordinate system. + * * @author Michael J. Simons * @since 6.0 */ @@ -35,15 +37,16 @@ public final class GeographicPoint3d extends AbstractPoint { } public double getLongitude() { - return coordinate.getX(); + return this.coordinate.getX(); } public double getLatitude() { - return coordinate.getY(); + return this.coordinate.getY(); } public double getHeight() { - return Objects.requireNonNull(coordinate.getZ(), "The underlying coordinate does not have a z-value (height)"); + return Objects.requireNonNull(this.coordinate.getZ(), + "The underlying coordinate does not have a z-value (height)"); } @Override @@ -51,4 +54,5 @@ public final class GeographicPoint3d extends AbstractPoint { return "GeographicPoint3d{" + "longitude=" + getLongitude() + ", latitude=" + getLatitude() + ", height=" + getHeight() + ", srid=" + getSrid() + '}'; } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/Neo4jPoint.java b/src/main/java/org/springframework/data/neo4j/types/Neo4jPoint.java index f0f923214..32ef57466 100644 --- a/src/main/java/org/springframework/data/neo4j/types/Neo4jPoint.java +++ b/src/main/java/org/springframework/data/neo4j/types/Neo4jPoint.java @@ -18,10 +18,13 @@ package org.springframework.data.neo4j.types; import org.apiguardian.api.API; /** - * A dedicated Neo4j point, that is aware of its nature, either being geographic or cartesian. While you can use this - * interface as an attribute type in your domain class, you should not mix different type of points on the same - * attribute of the same label. Queries will lead to inconsistent results. Use one of the concrete implementations. See - * Spatial values. + * A dedicated Neo4j point, that is aware of its nature, either being geographic or + * cartesian. While you can use this interface as an attribute type in your domain class, + * you should not mix different type of points on the same attribute of the same label. + * Queries will lead to inconsistent results. Use one of the concrete implementations. See + * Spatial + * values. * * @author Michael J. Simons * @since 6.0 @@ -30,7 +33,11 @@ import org.apiguardian.api.API; public sealed interface Neo4jPoint permits AbstractPoint { /** - * @return The Srid identifying the Coordinate Reference Systems (CRS) used by this point. + * Returns the Srid identifying the Coordinate Reference Systems (CRS) used by this + * point. + * @return the Srid identifying the Coordinate Reference Systems (CRS) used by this + * point */ Integer getSrid(); + } diff --git a/src/main/java/org/springframework/data/neo4j/types/PointBuilder.java b/src/main/java/org/springframework/data/neo4j/types/PointBuilder.java index 8be1e1f54..9a91146e2 100644 --- a/src/main/java/org/springframework/data/neo4j/types/PointBuilder.java +++ b/src/main/java/org/springframework/data/neo4j/types/PointBuilder.java @@ -18,6 +18,10 @@ package org.springframework.data.neo4j.types; import org.apiguardian.api.API; /** + * A builder for points, so that coordinates and optionally a coordinate system can be + * configured. Dependening on the coordinate system, {@link GeographicPoint2d} or + * {@link GeographicPoint3d} will be created, cartesian points otherwise. + * * @author Michael J. Simons * @since 6.0 */ @@ -26,22 +30,24 @@ public final class PointBuilder { private final int srid; - public static PointBuilder withSrid(int srid) { - return new PointBuilder(srid); - } - private PointBuilder(int srid) { this.srid = srid; } - public AbstractPoint build(Coordinate coordinate) { + public static PointBuilder withSrid(int srid) { + return new PointBuilder(srid); + } + + public Neo4jPoint build(Coordinate coordinate) { boolean is3d = coordinate.getZ() != null; - if (srid == CartesianPoint2d.SRID || srid == CartesianPoint3d.SRID) { + if (this.srid == CartesianPoint2d.SRID || this.srid == CartesianPoint3d.SRID) { return is3d ? new CartesianPoint3d(coordinate) : new CartesianPoint2d(coordinate); - } else { - return is3d ? new GeographicPoint3d(coordinate, srid) : new GeographicPoint2d(coordinate, srid); + } + else { + return is3d ? new GeographicPoint3d(coordinate, this.srid) : new GeographicPoint2d(coordinate, this.srid); } } + } diff --git a/src/main/java/org/springframework/data/neo4j/types/package-info.java b/src/main/java/org/springframework/data/neo4j/types/package-info.java index bf5280e9d..6d69d2e56 100644 --- a/src/main/java/org/springframework/data/neo4j/types/package-info.java +++ b/src/main/java/org/springframework/data/neo4j/types/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * Additional types provided by SDN. */ diff --git a/src/main/resources/META-INF/native-image/org.springframework.data/spring-data-neo4j/native-image.properties b/src/main/resources/META-INF/native-image/org.springframework.data/spring-data-neo4j/native-image.properties index a5c05f6cd..fd0dd3726 100644 --- a/src/main/resources/META-INF/native-image/org.springframework.data/spring-data-neo4j/native-image.properties +++ b/src/main/resources/META-INF/native-image/org.springframework.data/spring-data-neo4j/native-image.properties @@ -1 +1,17 @@ +# +# Copyright 2011-2025 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. +# + Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index 375b98c01..bbbacc86f 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -1 +1,17 @@ +# +# Copyright 2011-2025 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. +# + org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.neo4j.repository.core.Neo4jRepositoryFactory diff --git a/src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTest.java b/src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTests.java similarity index 52% rename from src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTest.java rename to src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTests.java index fcacf9424..676add73b 100644 --- a/src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTest.java +++ b/src/test/java/org/springframework/data/neo4j/architecture/ArchitectureTests.java @@ -31,13 +31,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.springframework.data.neo4j.config.Neo4jCdiConfigurationSupport; + /** * Architecture tests replacing the jQAssistant tests. */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ArchitectureTest { +public class ArchitectureTests { - private static final DescribedPredicate INTERNAL_API_PREDICATE = new DescribedPredicate<>("Is internal API") { + private static final DescribedPredicate INTERNAL_API_PREDICATE = new DescribedPredicate<>( + "Is internal API") { @Override public boolean apply(JavaClass input) { API.Status status = input.getAnnotationOfType(API.class).status(); @@ -49,34 +52,38 @@ public class ArchitectureTest { @BeforeAll void importCorePackage() { - sdnClasses = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .importPackages("org.springframework.data.neo4j.."); - + this.sdnClasses = new ClassFileImporter().withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("org.springframework.data.neo4j.."); } @DisplayName("Non abstract, public classes that are only part of internal API must be final") @Test void finalInternalAPIPublicClasses() { - ArchRule rule = ArchRuleDefinition.classes().that().areAnnotatedWith(API.class) - .and().arePublic() - .and().areTopLevelClasses() - .and(DescribedPredicate.not(HasModifiers.Predicates.modifier(JavaModifier.ABSTRACT))) - .and(INTERNAL_API_PREDICATE) - .should().haveModifier(JavaModifier.FINAL); - rule.check(sdnClasses); + ArchRule rule = ArchRuleDefinition.classes() + .that() + .areAnnotatedWith(API.class) + .and() + .arePublic() + .and() + .areTopLevelClasses() + .and(DescribedPredicate.not(HasModifiers.Predicates.modifier(JavaModifier.ABSTRACT))) + .and(INTERNAL_API_PREDICATE) + // CDI has issues with final classes + .and() + .areNotAssignableFrom(Neo4jCdiConfigurationSupport.class) + .should() + .haveModifier(JavaModifier.FINAL); + rule.check(this.sdnClasses); } - @DisplayName("@API Guardian annotations must not be used on fields") @Test void apiAnnotationsNotOnFields() { - ArchRule rule = ArchRuleDefinition.fields().should() - .notBeAnnotatedWith(API.class); + ArchRule rule = ArchRuleDefinition.fields().should().notBeAnnotatedWith(API.class); - rule.check(sdnClasses); + rule.check(this.sdnClasses); } @DisplayName("The mapping package must not depend on any other SDN packages than schema and convert") @@ -84,33 +91,42 @@ public class ArchitectureTest { void mappingPackageDependencies() { Architectures.layeredArchitecture() - .layer("mapping").definedBy("org.springframework.data.neo4j.core.mapping", "org.springframework.data.neo4j.core.mapping.callback") - .layer("schema or conversion").definedBy("org.springframework.data.neo4j.core.schema", "org.springframework.data.neo4j.core.convert") - .layer("everything outside SDN").definedBy(new DescribedPredicate<>("classes outside SDN") { - @Override - public boolean apply(JavaClass input) { - return !input.getPackageName().startsWith("org.springframework.data.neo4j"); - } - }) - .whereLayer("mapping").mayOnlyAccessLayers("schema or conversion", "everything outside SDN") - .withOptionalLayers(true) - .check(sdnClasses); + .layer("mapping") + .definedBy("org.springframework.data.neo4j.core.mapping", + "org.springframework.data.neo4j.core.mapping.callback") + .layer("schema or conversion") + .definedBy("org.springframework.data.neo4j.core.schema", "org.springframework.data.neo4j.core.convert") + .layer("everything outside SDN") + .definedBy(new DescribedPredicate<>("classes outside SDN") { + @Override + public boolean apply(JavaClass input) { + return !input.getPackageName().startsWith("org.springframework.data.neo4j"); + } + }) + .whereLayer("mapping") + .mayOnlyAccessLayers("schema or conversion", "everything outside SDN") + .withOptionalLayers(true) + .check(this.sdnClasses); } @DisplayName("The public support packages must not depend directly on the mapping package") @Test void publicPackagesMustNotDependOnMappingPackage() { - ArchRuleDefinition.classes().that().resideInAnyPackage("org.springframework.data.neo4j.core.convert", - "org.springframework.data.neo4j.core.schema", - "org.springframework.data.neo4j.core.support", - "org.springframework.data.neo4j.core.transaction") - .should() - .onlyDependOnClassesThat() - .resideOutsideOfPackages("org.springframework.data.neo4j.core.mapping", "org.springframework.data.neo4j.core.mapping.callback") - .orShould() - .dependOnClassesThat().haveFullyQualifiedName("org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty") - .check(sdnClasses); + ArchRuleDefinition.classes() + .that() + .resideInAnyPackage("org.springframework.data.neo4j.core.convert", + "org.springframework.data.neo4j.core.schema", "org.springframework.data.neo4j.core.support", + "org.springframework.data.neo4j.core.transaction") + .should() + .onlyDependOnClassesThat() + .resideOutsideOfPackages("org.springframework.data.neo4j.core.mapping", + "org.springframework.data.neo4j.core.mapping.callback") + .orShould() + .dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty") + .check(this.sdnClasses); } + } diff --git a/src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTest.java b/src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTests.java similarity index 73% rename from src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTest.java rename to src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTests.java index 3c0eb33e3..375c56a73 100644 --- a/src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTest.java +++ b/src/test/java/org/springframework/data/neo4j/config/Neo4jAuditingRegistrarTests.java @@ -15,35 +15,42 @@ */ package org.springframework.data.neo4j.config; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.type.AnnotationMetadata; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class Neo4jAuditingRegistrarTest { +class Neo4jAuditingRegistrarTests { - @Mock AnnotationMetadata metadata; - @Mock BeanDefinitionRegistry registry; + @Mock + AnnotationMetadata metadata; + + @Mock + BeanDefinitionRegistry registry; Neo4jAuditingRegistrar registrar = new Neo4jAuditingRegistrar(); @Test - public void rejectsNullAnnotationMetadata() { + void rejectsNullAnnotationMetadata() { - assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(null, registry)); + assertThatIllegalArgumentException() + .isThrownBy(() -> this.registrar.registerBeanDefinitions(null, this.registry)); } @Test - public void rejectsNullBeanDefinitionRegistry() { + void rejectsNullBeanDefinitionRegistry() { - assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(metadata, null)); + assertThatIllegalArgumentException() + .isThrownBy(() -> this.registrar.registerBeanDefinitions(this.metadata, null)); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTest.java b/src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTests.java similarity index 76% rename from src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTest.java rename to src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTests.java index 06605c359..ccec5ef5d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/DatabaseSelectionProviderTests.java @@ -15,23 +15,22 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Michael J. Simons - * @soundtrack Dr. Dre - The Chronic */ -class DatabaseSelectionProviderTest { +class DatabaseSelectionProviderTests { @Test void defaultProviderShallDefaultToNullDatabase() { assertThat(DatabaseSelectionProvider.getDefaultSelectionProvider().getDatabaseSelection()) - .isEqualTo(DatabaseSelection.undecided()); + .isEqualTo(DatabaseSelection.undecided()); } @Nested @@ -41,23 +40,26 @@ class DatabaseSelectionProviderTest { void databaseNameMustNotBeNull() { assertThatIllegalArgumentException() - .isThrownBy(() -> DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(null)) - .withMessage("The database name must not be null"); + .isThrownBy(() -> DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(null)) + .withMessage("The database name must not be null"); } @Test void databaseNameMustNotBeEmpty() { assertThatIllegalArgumentException() - .isThrownBy(() -> DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(" \t")) - .withMessage("The database name must not be empty"); + .isThrownBy(() -> DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(" \t")) + .withMessage("The database name must not be empty"); } @Test void shouldReturnConfiguredName() { - DatabaseSelectionProvider provider = DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("foobar"); + DatabaseSelectionProvider provider = DatabaseSelectionProvider + .createStaticDatabaseSelectionProvider("foobar"); assertThat(provider.getDatabaseSelection()).isEqualTo(DatabaseSelection.byName("foobar")); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java b/src/test/java/org/springframework/data/neo4j/core/NamedParametersTests.java similarity index 74% rename from src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java rename to src/test/java/org/springframework/data/neo4j/core/NamedParametersTests.java index 98cbefb90..29483d6af 100644 --- a/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/NamedParametersTests.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -27,10 +24,13 @@ import java.util.TreeMap; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Michael J. Simons */ -class NamedParametersTest { +class NamedParametersTests { @Test void shouldConvertCorrectListOfParametersIntoMap() { @@ -41,8 +41,9 @@ class NamedParametersTest { namedParameters.add("b", "Something"); namedParameters.add("c", null); - assertThat(namedParameters.get()).containsEntry("a", 1).containsEntry("b", "Something") - .containsEntry("c", null); + assertThat(namedParameters.get()).containsEntry("a", 1) + .containsEntry("b", "Something") + .containsEntry("c", null); } @Test @@ -54,8 +55,9 @@ class NamedParametersTest { namedParameters.add("a", 1); namedParameters.add("b", 1); namedParameters.add("a", 2); - }).withMessage( - "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); + }) + .withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); assertThatIllegalArgumentException().isThrownBy(() -> { NamedParameters namedParameters = new NamedParameters(); @@ -66,24 +68,27 @@ class NamedParametersTest { newValues.put("a", 2); namedParameters.addAll(newValues); - }).withMessage( - "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); + }) + .withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); assertThatIllegalArgumentException().isThrownBy(() -> { NamedParameters namedParameters = new NamedParameters(); namedParameters.add("a", null); namedParameters.add("a", 2); - }).withMessage( - "Duplicate parameter name: 'a' already in the list of named parameters with value 'null'. New value would be '2'"); + }) + .withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value 'null'. New value would be '2'"); assertThatIllegalArgumentException().isThrownBy(() -> { NamedParameters namedParameters = new NamedParameters(); namedParameters.add("a", 1); namedParameters.add("a", null); - }).withMessage( - "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be 'null'"); + }) + .withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be 'null'"); } @Test @@ -124,10 +129,7 @@ class NamedParametersTest { p.add("aKey", outer); String[] output = p.toString().split(System.lineSeparator()); - assertThat(output) - .containsExactly( - ":param aKey => {oma: 'Something', omb: {ims: 'Something else'}}" - ); + assertThat(output).containsExactly(":param aKey => {oma: 'Something', omb: {ims: 'Something else'}}"); } @Test @@ -142,10 +144,8 @@ class NamedParametersTest { p.add("aKey", outer); String[] output = p.toString().split(System.lineSeparator()); - assertThat(output) - .containsExactly( - ":param aKey => {oma: 'Something', omb: {ims: {imi: 'Embedded Thing'}}, omc: {ims: 'Something else'}}" - ); + assertThat(output).containsExactly( + ":param aKey => {oma: 'Something', omb: {ims: {imi: 'Embedded Thing'}}, omc: {ims: 'Something else'}}"); } @Test @@ -154,16 +154,14 @@ class NamedParametersTest { NamedParameters p = new NamedParameters(); p.add("a", Arrays.asList("Something", "Else")); p.add("l", Arrays.asList(1L, 2L, 3L)); - p.add("m", Arrays.asList( - Collections.singletonMap("a", "av"), Collections.singletonMap("b", Arrays.asList("A", "b")))); + p.add("m", Arrays.asList(Collections.singletonMap("a", "av"), + Collections.singletonMap("b", Arrays.asList("A", "b")))); String[] output = p.toString().split(System.lineSeparator()); - assertThat(output) - .containsExactly( - ":param a => ['Something', 'Else']", - ":param l => [1, 2, 3]", - ":param m => [{a: 'av'}, {b: ['A', 'b']}]" - ); + assertThat(output).containsExactly(":param a => ['Something', 'Else']", ":param l => [1, 2, 3]", + ":param m => [{a: 'av'}, {b: ['A', 'b']}]"); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTests.java similarity index 50% rename from src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java rename to src/test/java/org/springframework/data/neo4j/core/Neo4jClientTests.java index 12bd446df..af5734997 100644 --- a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTests.java @@ -15,17 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyMap; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - import java.lang.reflect.Method; import java.time.LocalDate; import java.util.ArrayList; @@ -61,41 +50,61 @@ import org.neo4j.driver.SessionConfig; import org.neo4j.driver.Values; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.util.ReflectionUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + /** * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class Neo4jClientTest { +class Neo4jClientTests { - @Mock private Driver driver; + @Mock + private Driver driver; private ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); - @Mock private Session session; + @Mock + private Session session; - @Mock private TypeSystem typeSystem; + @Mock + private TypeSystem typeSystem; - @Mock private Result result; + @Mock + private Result result; - @Mock private ResultSummary resultSummary; + @Mock + private ResultSummary resultSummary; - @Mock private Record record1; + @Mock + private Record record1; - @Mock private Record record2; + @Mock + private Record record2; void prepareMocks() { - when(driver.session(any(SessionConfig.class))).thenReturn(session); + given(this.driver.session(any(SessionConfig.class))).willReturn(this.session); - when(session.lastBookmarks()).thenReturn(Set.of(Mockito.mock(Bookmark.class))); + given(this.session.lastBookmarks()).willReturn(Set.of(Mockito.mock(Bookmark.class))); } @AfterEach void verifyNoMoreInteractionsWithMocks() { - verifyNoMoreInteractions(driver, session, result, resultSummary, record1, record2); + verifyNoMoreInteractions(this.driver, this.session, this.result, this.resultSummary, this.record1, + this.record2); } @Test // GH-2426 @@ -105,20 +114,21 @@ class Neo4jClientTest { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Optional> firstMatchingUser = client - .query(cypher) - .in("bikingDatabase") - .asUser("aUser") - .bind("Someone.*") - .to("name").fetch().first(); + Optional> firstMatchingUser = client.query(cypher) + .in("bikingDatabase") + .asUser("aUser") + .bind("Someone.*") + .to("name") + .fetch() + .first(); assertThat(firstMatchingUser).isPresent(); @@ -128,13 +138,13 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Someone.*"); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); } @Test // GH-2426 @@ -144,20 +154,21 @@ class Neo4jClientTest { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Optional> firstMatchingUser = client - .query(cypher) - .asUser("aUser") - .in("bikingDatabase") - .bind("Someone.*") - .to("name").fetch().first(); + Optional> firstMatchingUser = client.query(cypher) + .asUser("aUser") + .in("bikingDatabase") + .bind("Someone.*") + .to("name") + .fetch() + .first(); assertThat(firstMatchingUser).isPresent(); @@ -167,13 +178,13 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Someone.*"); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); } @Test // GH-2426 @@ -183,19 +194,20 @@ class Neo4jClientTest { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Optional> firstMatchingUser = client - .query(cypher) - .asUser("aUser") - .bind("Someone.*") - .to("name").fetch().first(); + Optional> firstMatchingUser = client.query(cypher) + .asUser("aUser") + .bind("Someone.*") + .to("name") + .fetch() + .first(); assertThat(firstMatchingUser).isPresent(); @@ -205,13 +217,13 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Someone.*"); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); } @Test @@ -220,24 +232,30 @@ class Neo4jClientTest { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); Map parameters = new HashMap<>(); parameters.put("bikeName", "M.*"); parameters.put("location", "Sweden"); String cypher = """ - MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip)\s - WHERE t.takenOn > $aDate AND b.name =~ $bikeName AND t.location = $location\s - RETURN b - """; + MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip)\s + WHERE t.takenOn > $aDate AND b.name =~ $bikeName AND t.location = $location\s + RETURN b + """; - Collection> usedBikes = client.query(cypher).bind("michael").to("name").bindAll(parameters) - .bind(LocalDate.of(2019, 1, 1)).to("aDate").fetch().all(); + Collection> usedBikes = client.query(cypher) + .bind("michael") + .to("name") + .bindAll(parameters) + .bind(LocalDate.of(2019, 1, 1)) + .to("aDate") + .fetch() + .all(); assertThat(usedBikes).hasSize(2); @@ -246,15 +264,15 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(parameters); expectedParameters.put("name", "michael"); expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(record2).asMap(); - verify(session).close(); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.record2).asMap(); + verify(this.session).close(); } @Test @@ -262,16 +280,20 @@ class Neo4jClientTest { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Optional> firstMatchingUser = client.query(cypher).in("bikingDatabase").bind("Someone.*") - .to("name").fetch().first(); + Optional> firstMatchingUser = client.query(cypher) + .in("bikingDatabase") + .bind("Someone.*") + .to("name") + .fetch() + .first(); assertThat(firstMatchingUser).isPresent(); @@ -280,19 +302,19 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Someone.*"); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); } @Test void databaseSelectionShouldPreventIllegalValues() { - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); assertThat(client.query("RETURN 1").in(null)).isNotNull(); assertThat(client.query("RETURN 1").in("foobar")).isNotNull(); @@ -300,7 +322,7 @@ class Neo4jClientTest { String[] invalidDatabaseNames = { "", " ", "\t" }; for (String invalidDatabaseName : invalidDatabaseNames) { assertThatIllegalArgumentException() - .isThrownBy(() -> client.delegateTo(r -> Optional.empty()).in(invalidDatabaseName)); + .isThrownBy(() -> client.delegateTo(r -> Optional.empty()).in(invalidDatabaseName)); } for (String invalidDatabaseName : invalidDatabaseNames) { @@ -311,226 +333,40 @@ class Neo4jClientTest { @Test // GH-2159 void databaseSelectionBeanShouldGetRespectedIfExisting() { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.stream()).willReturn(Stream.of(this.record1, this.record2)); + given(this.result.consume()).willReturn(this.resultSummary); String databaseName = "customDatabaseSelection"; DatabaseSelectionProvider databaseSelection = DatabaseSelectionProvider - .createStaticDatabaseSelectionProvider(databaseName); + .createStaticDatabaseSelectionProvider(databaseName); - Neo4jClient client = Neo4jClient.create(driver, databaseSelection); + Neo4jClient client = Neo4jClient.create(this.driver, databaseSelection); String query = "RETURN 1"; client.query(query).fetch().first(); verifyDatabaseSelection(databaseName); - verify(session).run(eq(query), anyMap()); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); + verify(this.session).run(eq(query), anyMap()); + verify(this.result).stream(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); } - @Nested - @DisplayName("Callback handling should feel good") - class CallbackHandlingShouldFeelGood { - - @Test - void withDefaultDatabase() { - - prepareMocks(); - - Neo4jClient client = Neo4jClient.create(driver); - Optional singleResult = client.delegateTo(runner -> Optional.of(42)).run(); - - assertThat(singleResult).isPresent().hasValue(42); - - verifyDatabaseSelection(null); - - verify(session).close(); - } - - @Test - void withDatabase() { - - prepareMocks(); - - Neo4jClient client = Neo4jClient.create(driver); - Optional singleResult = client.delegateTo(runner -> Optional.of(42)).in("aDatabase").run(); - - assertThat(singleResult).isPresent().hasValue(42); - - verifyDatabaseSelection("aDatabase"); - - verify(session).close(); - } - - @Test // GH-2369 - void databaseSelectionShouldBePropagatedToDelegate() { - - prepareMocks(); - - String databaseName = "aDatabase"; - DatabaseSelectionProvider databaseSelection = DatabaseSelectionProvider - .createStaticDatabaseSelectionProvider(databaseName); - - Neo4jClient client = Neo4jClient.create(driver, databaseSelection); - Optional singleResult = client.delegateTo(runner -> Optional.of(42)).run(); - - assertThat(singleResult).isPresent().hasValue(42); - - verifyDatabaseSelection("aDatabase"); - - verify(session).close(); - } - } - - @Nested - @DisplayName("Mapping should feel good") - class MappingShouldFeelGood { - - @Test - void reading() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1)); - when(result.consume()).thenReturn(resultSummary); - when(record1.get("name")).thenReturn(Values.value("michael")); - - Neo4jClient client = Neo4jClient.create(driver); - - String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) RETURN o, collect(b) as bikes"; - - BikeOwnerReader mappingFunction = new BikeOwnerReader(); - Collection bikeOwners = client.query(cypher).bind("michael").to("name").fetchAs(BikeOwner.class) - .mappedBy(mappingFunction).all(); - - assertThat(bikeOwners).hasSize(1).first().hasFieldOrPropertyWithValue("name", "michael"); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "michael"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).get("name"); - verify(session).close(); - } - - @Test - void shouldApplyNullChecksDuringReading() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.stream()).thenReturn(Stream.of(record1, record2)); - when(result.consume()).thenReturn(resultSummary); - when(record1.get("name")).thenReturn(Values.value("michael")); - - Neo4jClient client = Neo4jClient.create(driver); - - Collection owners = client.query("MATCH (n) RETURN n").fetchAs(BikeOwner.class) - .mappedBy((t, r) -> { - if (r == record1) { - return new BikeOwner(r.get("name").asString(), Collections.emptyList()); - } else { - return null; - } - }).all(); - assertThat(owners).hasSize(1); - verifyDatabaseSelection(null); - - verify(session).run(eq("MATCH (n) RETURN n"), MockitoHamcrest.argThat(new MapAssertionMatcher(Collections.emptyMap()))); - verify(result).stream(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).get("name"); - verify(session).close(); - } - - @Test - void writing() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.consume()).thenReturn(resultSummary); - - Neo4jClient client = Neo4jClient.create(driver); - - BikeOwner michael = new BikeOwner("Michael", Arrays.asList(new Bike("Road"), new Bike("MTB"))); - String cypher = """ - MERGE (u:User {name: 'Michael'}) - WITH u UNWIND $bikes as bike - MERGE (b:Bike {name: bike}) MERGE (u) - [o:OWNS] -> (b) - """; - ResultSummary summary = client.query(cypher).bind(michael).with(new BikeOwnerBinder()).run(); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Michael"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).close(); - } - - @Test - @DisplayName("Some automatic conversion is ok") - void automaticConversion() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.hasNext()).thenReturn(true); - when(result.single()).thenReturn(record1); - when(result.consume()).thenReturn(resultSummary); - when(record1.size()).thenReturn(1); - when(record1.get(0)).thenReturn(Values.value(23L)); - - Neo4jClient client = Neo4jClient.create(driver); - - String cypher = "MATCH (b:Bike) RETURN count(b)"; - Optional numberOfBikes = client.query(cypher).fetchAs(Long.class).one(); - - assertThat(numberOfBikes).isPresent().hasValue(23L); - - verifyDatabaseSelection(null); - - verify(session).run(eq(cypher), anyMap()); - verify(result).hasNext(); - verify(result).single(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).close(); - } - } - @Test @DisplayName("Queries that return nothing should fit in") void queriesWithoutResultShouldFitInAsWell() { prepareMocks(); - when(session.run(anyString(), anyMap())).thenReturn(result); - when(result.consume()).thenReturn(resultSummary); + given(this.session.run(anyString(), anyMap())).willReturn(this.result); + given(this.result.consume()).willReturn(this.resultSummary); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); String cypher = "DETACH DELETE (b) WHERE name = $name"; @@ -541,11 +377,40 @@ class Neo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "fixie"); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).close(); + verify(this.session).run(eq(cypher), MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.session).close(); + } + + void verifyDatabaseSelection(String targetDatabase) { + + verify(this.driver).session(this.configArgumentCaptor.capture()); + SessionConfig config = this.configArgumentCaptor.getValue(); + + if (targetDatabase != null) { + assertThat(config.database()).isPresent().contains(targetDatabase); + } + else { + assertThat(config.database()).isEmpty(); + } + } + + void verifyUserSelection(String aUser) { + + verify(this.driver).session(this.configArgumentCaptor.capture()); + SessionConfig config = this.configArgumentCaptor.getValue(); + + // We assume the driver supports this before the test + final Method impersonatedUser = ReflectionUtils.findMethod(SessionConfig.class, "impersonatedUser"); + if (aUser != null) { + Optional optionalValue = (Optional) ReflectionUtils.invokeMethod(impersonatedUser, config); + assertThat(optionalValue).isPresent().contains(aUser); + } + else { + assertThat(config.database()).isEmpty(); + } } static class BikeOwner { @@ -559,13 +424,14 @@ class Neo4jClientTest { this.bikes = new ArrayList<>(bikes); } - public String getName() { - return name; + String getName() { + return this.name; } - public List getBikes() { - return Collections.unmodifiableList(bikes); + List getBikes() { + return Collections.unmodifiableList(this.bikes); } + } static class Bike { @@ -576,9 +442,10 @@ class Neo4jClientTest { this.name = name; } - public String getName() { - return name; + String getName() { + return this.name; } + } static class BikeOwnerReader implements BiFunction { @@ -587,6 +454,7 @@ class Neo4jClientTest { public BikeOwner apply(TypeSystem typeSystem, Record record) { return new BikeOwner(record.get("name").asString(), Collections.emptyList()); } + } static class BikeOwnerBinder implements Function> { @@ -599,36 +467,11 @@ class Neo4jClientTest { mappedValues.put("name", bikeOwner.getName()); return mappedValues; } - } - void verifyDatabaseSelection(String targetDatabase) { - - verify(driver).session(configArgumentCaptor.capture()); - SessionConfig config = configArgumentCaptor.getValue(); - - if (targetDatabase != null) { - assertThat(config.database()).isPresent().contains(targetDatabase); - } else { - assertThat(config.database()).isEmpty(); - } - } - - void verifyUserSelection(String aUser) { - - verify(driver).session(configArgumentCaptor.capture()); - SessionConfig config = configArgumentCaptor.getValue(); - - // We assume the driver supports this before the test - final Method impersonatedUser = ReflectionUtils.findMethod(SessionConfig.class, "impersonatedUser"); - if (aUser != null) { - Optional optionalValue = (Optional) ReflectionUtils.invokeMethod(impersonatedUser, config); - assertThat(optionalValue).isPresent().contains(aUser); - } else { - assertThat(config.database()).isEmpty(); - } } static class MapAssertionMatcher extends AssertionMatcher> { + private final Map expectedParameters; MapAssertionMatcher(Map expectedParameters) { @@ -637,7 +480,208 @@ class Neo4jClientTest { @Override public void assertion(Map actual) { - assertThat(actual).containsAllEntriesOf(expectedParameters); + assertThat(actual).containsAllEntriesOf(this.expectedParameters); } + } + + @Nested + @DisplayName("Callback handling should feel good") + class CallbackHandlingShouldFeelGood { + + @Test + void withDefaultDatabase() { + + prepareMocks(); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + Optional singleResult = client.delegateTo(runner -> Optional.of(42)).run(); + + assertThat(singleResult).isPresent().hasValue(42); + + verifyDatabaseSelection(null); + + verify(Neo4jClientTests.this.session).close(); + } + + @Test + void withDatabase() { + + prepareMocks(); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + Optional singleResult = client.delegateTo(runner -> Optional.of(42)).in("aDatabase").run(); + + assertThat(singleResult).isPresent().hasValue(42); + + verifyDatabaseSelection("aDatabase"); + + verify(Neo4jClientTests.this.session).close(); + } + + @Test // GH-2369 + void databaseSelectionShouldBePropagatedToDelegate() { + + prepareMocks(); + + String databaseName = "aDatabase"; + DatabaseSelectionProvider databaseSelection = DatabaseSelectionProvider + .createStaticDatabaseSelectionProvider(databaseName); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver, databaseSelection); + Optional singleResult = client.delegateTo(runner -> Optional.of(42)).run(); + + assertThat(singleResult).isPresent().hasValue(42); + + verifyDatabaseSelection("aDatabase"); + + verify(Neo4jClientTests.this.session).close(); + } + + } + + @Nested + @DisplayName("Mapping should feel good") + class MappingShouldFeelGood { + + @Test + void reading() { + + prepareMocks(); + + given(Neo4jClientTests.this.session.run(anyString(), anyMap())).willReturn(Neo4jClientTests.this.result); + given(Neo4jClientTests.this.result.stream()).willReturn(Stream.of(Neo4jClientTests.this.record1)); + given(Neo4jClientTests.this.result.consume()).willReturn(Neo4jClientTests.this.resultSummary); + given(Neo4jClientTests.this.record1.get("name")).willReturn(Values.value("michael")); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) RETURN o, collect(b) as bikes"; + + BikeOwnerReader mappingFunction = new BikeOwnerReader(); + Collection bikeOwners = client.query(cypher) + .bind("michael") + .to("name") + .fetchAs(BikeOwner.class) + .mappedBy(mappingFunction) + .all(); + + assertThat(bikeOwners).hasSize(1).first().hasFieldOrPropertyWithValue("name", "michael"); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "michael"); + + verify(Neo4jClientTests.this.session).run(eq(cypher), + MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(Neo4jClientTests.this.result).stream(); + verify(Neo4jClientTests.this.result).consume(); + verify(Neo4jClientTests.this.resultSummary).notifications(); + verify(Neo4jClientTests.this.resultSummary).hasPlan(); + verify(Neo4jClientTests.this.record1).get("name"); + verify(Neo4jClientTests.this.session).close(); + } + + @Test + void shouldApplyNullChecksDuringReading() { + + prepareMocks(); + + given(Neo4jClientTests.this.session.run(anyString(), anyMap())).willReturn(Neo4jClientTests.this.result); + given(Neo4jClientTests.this.result.stream()) + .willReturn(Stream.of(Neo4jClientTests.this.record1, Neo4jClientTests.this.record2)); + given(Neo4jClientTests.this.result.consume()).willReturn(Neo4jClientTests.this.resultSummary); + given(Neo4jClientTests.this.record1.get("name")).willReturn(Values.value("michael")); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + + Collection owners = client.query("MATCH (n) RETURN n") + .fetchAs(BikeOwner.class) + .mappedBy((t, r) -> { + if (r == Neo4jClientTests.this.record1) { + return new BikeOwner(r.get("name").asString(), Collections.emptyList()); + } + else { + return null; + } + }) + .all(); + assertThat(owners).hasSize(1); + verifyDatabaseSelection(null); + + verify(Neo4jClientTests.this.session).run(eq("MATCH (n) RETURN n"), + MockitoHamcrest.argThat(new MapAssertionMatcher(Collections.emptyMap()))); + verify(Neo4jClientTests.this.result).stream(); + verify(Neo4jClientTests.this.result).consume(); + verify(Neo4jClientTests.this.resultSummary).notifications(); + verify(Neo4jClientTests.this.resultSummary).hasPlan(); + verify(Neo4jClientTests.this.record1).get("name"); + verify(Neo4jClientTests.this.session).close(); + } + + @Test + void writing() { + + prepareMocks(); + + given(Neo4jClientTests.this.session.run(anyString(), anyMap())).willReturn(Neo4jClientTests.this.result); + given(Neo4jClientTests.this.result.consume()).willReturn(Neo4jClientTests.this.resultSummary); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + + BikeOwner michael = new BikeOwner("Michael", Arrays.asList(new Bike("Road"), new Bike("MTB"))); + String cypher = """ + MERGE (u:User {name: 'Michael'}) + WITH u UNWIND $bikes as bike + MERGE (b:Bike {name: bike}) MERGE (u) - [o:OWNS] -> (b) + """; + ResultSummary summary = client.query(cypher).bind(michael).with(new BikeOwnerBinder()).run(); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Michael"); + + verify(Neo4jClientTests.this.session).run(eq(cypher), + MockitoHamcrest.argThat(new MapAssertionMatcher(expectedParameters))); + verify(Neo4jClientTests.this.result).consume(); + verify(Neo4jClientTests.this.resultSummary).notifications(); + verify(Neo4jClientTests.this.resultSummary).hasPlan(); + verify(Neo4jClientTests.this.session).close(); + } + + @Test + @DisplayName("Some automatic conversion is ok") + void automaticConversion() { + + prepareMocks(); + + given(Neo4jClientTests.this.session.run(anyString(), anyMap())).willReturn(Neo4jClientTests.this.result); + given(Neo4jClientTests.this.result.hasNext()).willReturn(true); + given(Neo4jClientTests.this.result.single()).willReturn(Neo4jClientTests.this.record1); + given(Neo4jClientTests.this.result.consume()).willReturn(Neo4jClientTests.this.resultSummary); + given(Neo4jClientTests.this.record1.size()).willReturn(1); + given(Neo4jClientTests.this.record1.get(0)).willReturn(Values.value(23L)); + + Neo4jClient client = Neo4jClient.create(Neo4jClientTests.this.driver); + + String cypher = "MATCH (b:Bike) RETURN count(b)"; + Optional numberOfBikes = client.query(cypher).fetchAs(Long.class).one(); + + assertThat(numberOfBikes).isPresent().hasValue(23L); + + verifyDatabaseSelection(null); + + verify(Neo4jClientTests.this.session).run(eq(cypher), anyMap()); + verify(Neo4jClientTests.this.result).hasNext(); + verify(Neo4jClientTests.this.result).single(); + verify(Neo4jClientTests.this.result).consume(); + verify(Neo4jClientTests.this.resultSummary).notifications(); + verify(Neo4jClientTests.this.resultSummary).hasPlan(); + verify(Neo4jClientTests.this.session).close(); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTest.java b/src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTests.java similarity index 83% rename from src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTest.java rename to src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTests.java index f8754155d..60fae7159 100644 --- a/src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/Neo4jPersistenceExceptionTranslatorTests.java @@ -15,28 +15,30 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.value.LossyCoercion; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class Neo4jPersistenceExceptionTranslatorTest { +class Neo4jPersistenceExceptionTranslatorTests { @Test void shouldHandleNullErrorCode() { Neo4jPersistenceExceptionTranslator translator = new Neo4jPersistenceExceptionTranslator(); - DataAccessException dataAccessException = translator.translateExceptionIfPossible(new LossyCoercion("Long", "Int")); + DataAccessException dataAccessException = translator + .translateExceptionIfPossible(new LossyCoercion("Long", "Int")); assertThat(dataAccessException).isNotNull().isInstanceOf(InvalidDataAccessApiUsageException.class); assertThat(dataAccessException.getMessage()) - .startsWith("Cannot coerce Long to Int without losing precision; Error code 'N/A'"); + .startsWith("Cannot coerce Long to Int without losing precision; Error code 'N/A'"); } @Test @@ -47,6 +49,7 @@ class Neo4jPersistenceExceptionTranslatorTest { new ClientException("Neo.ClientError.Statement.EntityNotFound", "Something went wrong.")); assertThat(dataAccessException).isNotNull().isInstanceOf(InvalidDataAccessResourceUsageException.class); assertThat(dataAccessException.getMessage()) - .startsWith("Something went wrong.; Error code 'Neo.ClientError.Statement.EntityNotFound'"); + .startsWith("Something went wrong.; Error code 'Neo.ClientError.Statement.EntityNotFound'"); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java deleted file mode 100644 index 466d86e7a..000000000 --- a/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ /dev/null @@ -1,577 +0,0 @@ -/* - * Copyright 2011-2025 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.core; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.hamcrest.MockitoHamcrest; -import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.Bookmark; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Record; -import org.neo4j.driver.SessionConfig; -import org.neo4j.driver.Values; -import org.neo4j.driver.reactivestreams.ReactiveResult; -import org.neo4j.driver.reactivestreams.ReactiveSession; -import org.neo4j.driver.summary.ResultSummary; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.util.ReflectionUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; - -import java.lang.reflect.Method; -import java.time.LocalDate; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -/** - * @author Michael J. Simons - */ -@ExtendWith(MockitoExtension.class) -class ReactiveNeo4jClientTest { - - @Mock private Driver driver; - - private ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); - - @Mock private ReactiveSession session; - - @Mock private ReactiveResult result; - - @Mock private ResultSummary resultSummary; - - @Mock private Record record1; - - @Mock private Record record2; - - void prepareMocks() { - - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).thenReturn(session); - - when(session.lastBookmarks()).thenReturn(Set.of(Mockito.mock(Bookmark.class))); - when(session.close()).thenReturn(Mono.empty()); - } - - @AfterEach - void verifyNoMoreInteractionsWithMocks() { - verifyNoMoreInteractions(driver, session, result, resultSummary, record1, record2); - } - - @Test // GH-2426 - void databaseSelectionShouldWorkBeforeAsUser() { - - assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2).publishOn(Schedulers.single())); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Mono> firstMatchingUser = client - .query(cypher) - .in("bikingDatabase") - .asUser("aUser") - .bind("Someone.*") - .to("name") - .fetch().first(); - - StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); - - verifyDatabaseSelection("bikingDatabase"); - verifyUserSelection("aUser"); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Someone.*"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); - } - - @Test // GH-2426 - void databaseSelectionShouldWorkAfterAsUser() { - - assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2).publishOn(Schedulers.single())); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Mono> firstMatchingUser = client - .query(cypher) - .asUser("aUser") - .in("bikingDatabase") - .bind("Someone.*") - .to("name") - .fetch().first(); - - StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); - - verifyDatabaseSelection("bikingDatabase"); - verifyUserSelection("aUser"); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Someone.*"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); - } - - @Test // GH-2426 - void userSelectionShouldWork() { - - assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2).publishOn(Schedulers.single())); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Mono> firstMatchingUser = client - .query(cypher) - .asUser("aUser") - .bind("Someone.*") - .to("name") - .fetch().first(); - - StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); - - verifyDatabaseSelection(null); - verifyUserSelection("aUser"); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Someone.*"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); - } - - @Test - @DisplayName("Creation of queries and binding parameters should feel natural") - void queryCreationShouldFeelGood() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - Map parameters = new HashMap<>(); - parameters.put("bikeName", "M.*"); - parameters.put("location", "Sweden"); - - String cypher = """ - MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) - WHERE t.takenOn > $aDate - AND b.name =~ $bikeName - AND t.location = $location RETURN b - """; - - Flux> usedBikes = client.query(cypher).bind("michael").to("name").bindAll(parameters) - .bind(LocalDate.of(2019, 1, 1)).to("aDate").fetch().all(); - - StepVerifier.create(usedBikes).expectNextCount(2L).verifyComplete(); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.putAll(parameters); - expectedParameters.put("name", "michael"); - expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(record2).asMap(); - verify(session).close(); - } - - @Test - void databaseSelectionShouldBePossibleOnlyOnce() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2).publishOn(Schedulers.single())); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (u:User) WHERE u.name =~ $name"; - Mono> firstMatchingUser = client.query(cypher).in("bikingDatabase").bind("Someone.*").to("name") - .fetch().first(); - - StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); - - verifyDatabaseSelection("bikingDatabase"); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Someone.*"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); - } - - @Test - void databaseSelectionShouldPreventIllegalValues() { - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - assertThat(client.query("RETURN 1").in(null)).isNotNull(); - assertThat(client.query("RETURN 1").in("foobar")).isNotNull(); - - String[] invalidDatabaseNames = { "", " ", "\t" }; - for (String invalidDatabaseName : invalidDatabaseNames) { - assertThatIllegalArgumentException() - .isThrownBy(() -> client.delegateTo(r -> Mono.empty()).in(invalidDatabaseName)); - } - - for (String invalidDatabaseName : invalidDatabaseNames) { - assertThatIllegalArgumentException().isThrownBy(() -> client.query("RETURN 1").in(invalidDatabaseName)); - } - } - - @Test // GH-2159 - void databaseSelectionBeanShouldGetRespectedIfExisting() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2).publishOn(Schedulers.single())); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - String databaseName = "customDatabaseSelection"; - String cypher = "RETURN 1"; - ReactiveDatabaseSelectionProvider databaseSelection = ReactiveDatabaseSelectionProvider - .createStaticDatabaseSelectionProvider(databaseName); - - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver, databaseSelection); - - StepVerifier.create(client.query(cypher).fetch().first()) - .expectNextCount(1L) - .verifyComplete(); - - verifyDatabaseSelection(databaseName); - - verify(session).run(eq(cypher), anyMap()); - verify(result).records(); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).asMap(); - verify(session).close(); - } - - @Test // GH-2369 - void databaseSelectionShouldBePropagatedToDelegate() { - - prepareMocks(); - - String databaseName = "aDatabase"; - ReactiveDatabaseSelectionProvider databaseSelection = ReactiveDatabaseSelectionProvider - .createStaticDatabaseSelectionProvider(databaseName); - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver, databaseSelection); - Mono singleResult = client.delegateTo(runner -> Mono.just(21)).run(); - - StepVerifier.create(singleResult).expectNext(21).verifyComplete(); - - verifyDatabaseSelection("aDatabase"); - - verify(session).close(); - } - - @Nested - @DisplayName("Callback handling should feel good") - class CallbackHandlingShouldFeelGood { - - @Test - void withDefaultDatabase() { - - prepareMocks(); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - Mono singleResult = client.delegateTo(runner -> Mono.just(21)).run(); - - StepVerifier.create(singleResult).expectNext(21).verifyComplete(); - - verifyDatabaseSelection(null); - - verify(session).close(); - } - - @Test - void withDatabase() { - - prepareMocks(); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - Mono singleResult = client.delegateTo(runner -> Mono.just(21)).in("aDatabase").run(); - - StepVerifier.create(singleResult).expectNext(21).verifyComplete(); - - verifyDatabaseSelection("aDatabase"); - - verify(session).close(); - } - } - - @Nested - @DisplayName("Mapping should feel good") - class MappingShouldFeelGood { - - @Test - void reading() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - when(record1.get("name")).thenReturn(Values.value("michael")); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) RETURN o, collect(b) as bikes"; - - Neo4jClientTest.BikeOwnerReader mappingFunction = new Neo4jClientTest.BikeOwnerReader(); - Flux bikeOwners = client.query(cypher).bind("michael").to("name") - .fetchAs(Neo4jClientTest.BikeOwner.class).mappedBy(mappingFunction).all(); - - StepVerifier.create(bikeOwners).expectNextMatches(o -> o.getName().equals("michael")).verifyComplete(); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "michael"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).records(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).get("name"); - verify(session).close(); - } - - @Test - void shouldApplyNullChecksDuringReading() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1, record2)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - when(record1.get("name")).thenReturn(Values.value("michael")); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - Flux bikeOwners = client.query("MATCH (n) RETURN n") - .fetchAs(Neo4jClientTest.BikeOwner.class).mappedBy((t, r) -> { - if (r == record1) { - return new Neo4jClientTest.BikeOwner(r.get("name").asString(), Collections.emptyList()); - } else { - return null; - } - }).all(); - - StepVerifier.create(bikeOwners).expectNextCount(1).verifyComplete(); - - verifyDatabaseSelection(null); - - verify(session).run(eq("MATCH (n) RETURN n"), - MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(Collections.emptyMap()))); - verify(result).records(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(record1).get("name"); - verify(session).close(); - } - - @Test - void writing() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - Neo4jClientTest.BikeOwner michael = new Neo4jClientTest.BikeOwner("Michael", - Arrays.asList(new Neo4jClientTest.Bike("Road"), new Neo4jClientTest.Bike("MTB"))); - String cypher = "MERGE (u:User {name: 'Michael'}) WITH u UNWIND $bikes as bike MERGE (b:Bike {name: bike}) MERGE (u) - [o:OWNS] -> (b) "; - - Mono summary = client.query(cypher).bind(michael).with(new Neo4jClientTest.BikeOwnerBinder()) - .run(); - - StepVerifier.create(summary).expectNext(resultSummary).verifyComplete(); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "Michael"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).close(); - } - - @Test - @DisplayName("Some automatic conversion is ok") - void automaticConversion() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.records()).thenReturn(Flux.just(record1)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - when(record1.size()).thenReturn(1); - when(record1.get(0)).thenReturn(Values.value(23L)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "MATCH (b:Bike) RETURN count(b)"; - Mono numberOfBikes = client.query(cypher).fetchAs(Long.class).one(); - - StepVerifier.create(numberOfBikes).expectNext(23L).verifyComplete(); - - verifyDatabaseSelection(null); - - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).run(eq(cypher), anyMap()); - verify(session).close(); - } - } - - @Test - @DisplayName("Queries that return nothing should fit in") - void queriesWithoutResultShouldFitInAsWell() { - - prepareMocks(); - - when(session.run(anyString(), anyMap())).thenReturn(Mono.just(result)); - when(result.consume()).thenReturn(Mono.just(resultSummary)); - - ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); - - String cypher = "DETACH DELETE (b) WHERE name = $name"; - - Mono deletionResult = client.query(cypher).bind("fixie").to("name").run(); - - StepVerifier.create(deletionResult).expectNext(resultSummary).verifyComplete(); - - verifyDatabaseSelection(null); - - Map expectedParameters = new HashMap<>(); - expectedParameters.put("name", "fixie"); - - verify(session).run(eq(cypher), MockitoHamcrest.argThat(new Neo4jClientTest.MapAssertionMatcher(expectedParameters))); - verify(result).consume(); - verify(resultSummary).notifications(); - verify(resultSummary).hasPlan(); - verify(session).close(); - } - - void verifyDatabaseSelection(String targetDatabase) { - - verify(driver).session(eq(ReactiveSession.class), configArgumentCaptor.capture()); - SessionConfig config = configArgumentCaptor.getValue(); - - if (targetDatabase != null) { - assertThat(config.database()).isPresent().contains(targetDatabase); - } else { - assertThat(config.database()).isEmpty(); - } - } - - void verifyUserSelection(String aUser) { - - verify(driver).session(eq(ReactiveSession.class), configArgumentCaptor.capture()); - SessionConfig config = configArgumentCaptor.getValue(); - - // We assume the driver supports this before the test - final Method impersonatedUser = ReflectionUtils.findMethod(SessionConfig.class, "impersonatedUser"); - if (aUser != null) { - Optional optionalValue = (Optional) ReflectionUtils.invokeMethod(impersonatedUser, config); - assertThat(optionalValue).isPresent().contains(aUser); - } else { - assertThat(config.database()).isEmpty(); - } - } -} diff --git a/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTests.java b/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTests.java new file mode 100644 index 000000000..0964ddee5 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTests.java @@ -0,0 +1,625 @@ +/* + * Copyright 2011-2025 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.core; + +import java.lang.reflect.Method; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.hamcrest.MockitoHamcrest; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.Bookmark; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; +import org.neo4j.driver.reactivestreams.ReactiveResult; +import org.neo4j.driver.reactivestreams.ReactiveSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class ReactiveNeo4jClientTests { + + @Mock + private Driver driver; + + private ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); + + @Mock + private ReactiveSession session; + + @Mock + private ReactiveResult result; + + @Mock + private ResultSummary resultSummary; + + @Mock + private Record record1; + + @Mock + private Record record2; + + void prepareMocks() { + + given(this.driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).willReturn(this.session); + + given(this.session.lastBookmarks()).willReturn(Set.of(Mockito.mock(Bookmark.class))); + given(this.session.close()).willReturn(Mono.empty()); + } + + @AfterEach + void verifyNoMoreInteractionsWithMocks() { + verifyNoMoreInteractions(this.driver, this.session, this.result, this.resultSummary, this.record1, + this.record2); + } + + @Test // GH-2426 + void databaseSelectionShouldWorkBeforeAsUser() { + + assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2).publishOn(Schedulers.single())); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + Mono> firstMatchingUser = client.query(cypher) + .in("bikingDatabase") + .asUser("aUser") + .bind("Someone.*") + .to("name") + .fetch() + .first(); + + StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); + + verifyDatabaseSelection("bikingDatabase"); + verifyUserSelection("aUser"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); + } + + @Test // GH-2426 + void databaseSelectionShouldWorkAfterAsUser() { + + assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2).publishOn(Schedulers.single())); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + Mono> firstMatchingUser = client.query(cypher) + .asUser("aUser") + .in("bikingDatabase") + .bind("Someone.*") + .to("name") + .fetch() + .first(); + + StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); + + verifyDatabaseSelection("bikingDatabase"); + verifyUserSelection("aUser"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); + } + + @Test // GH-2426 + void userSelectionShouldWork() { + + assumeThat(Neo4jTransactionUtils.driverSupportsImpersonation()).isTrue(); + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2).publishOn(Schedulers.single())); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + Mono> firstMatchingUser = client.query(cypher) + .asUser("aUser") + .bind("Someone.*") + .to("name") + .fetch() + .first(); + + StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); + + verifyDatabaseSelection(null); + verifyUserSelection("aUser"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); + } + + @Test + @DisplayName("Creation of queries and binding parameters should feel natural") + void queryCreationShouldFeelGood() { + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2)); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + Map parameters = new HashMap<>(); + parameters.put("bikeName", "M.*"); + parameters.put("location", "Sweden"); + + String cypher = """ + MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) + WHERE t.takenOn > $aDate + AND b.name =~ $bikeName + AND t.location = $location RETURN b + """; + + Flux> usedBikes = client.query(cypher) + .bind("michael") + .to("name") + .bindAll(parameters) + .bind(LocalDate.of(2019, 1, 1)) + .to("aDate") + .fetch() + .all(); + + StepVerifier.create(usedBikes).expectNextCount(2L).verifyComplete(); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.putAll(parameters); + expectedParameters.put("name", "michael"); + expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.record2).asMap(); + verify(this.session).close(); + } + + @Test + void databaseSelectionShouldBePossibleOnlyOnce() { + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2).publishOn(Schedulers.single())); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + Mono> firstMatchingUser = client.query(cypher) + .in("bikingDatabase") + .bind("Someone.*") + .to("name") + .fetch() + .first(); + + StepVerifier.create(firstMatchingUser).expectNextCount(1L).verifyComplete(); + + verifyDatabaseSelection("bikingDatabase"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); + } + + @Test + void databaseSelectionShouldPreventIllegalValues() { + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + assertThat(client.query("RETURN 1").in(null)).isNotNull(); + assertThat(client.query("RETURN 1").in("foobar")).isNotNull(); + + String[] invalidDatabaseNames = { "", " ", "\t" }; + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException() + .isThrownBy(() -> client.delegateTo(r -> Mono.empty()).in(invalidDatabaseName)); + } + + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException().isThrownBy(() -> client.query("RETURN 1").in(invalidDatabaseName)); + } + } + + @Test // GH-2159 + void databaseSelectionBeanShouldGetRespectedIfExisting() { + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.records()).willReturn(Flux.just(this.record1, this.record2).publishOn(Schedulers.single())); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + String databaseName = "customDatabaseSelection"; + String cypher = "RETURN 1"; + ReactiveDatabaseSelectionProvider databaseSelection = ReactiveDatabaseSelectionProvider + .createStaticDatabaseSelectionProvider(databaseName); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver, databaseSelection); + + StepVerifier.create(client.query(cypher).fetch().first()).expectNextCount(1L).verifyComplete(); + + verifyDatabaseSelection(databaseName); + + verify(this.session).run(eq(cypher), anyMap()); + verify(this.result).records(); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.record1).asMap(); + verify(this.session).close(); + } + + @Test // GH-2369 + void databaseSelectionShouldBePropagatedToDelegate() { + + prepareMocks(); + + String databaseName = "aDatabase"; + ReactiveDatabaseSelectionProvider databaseSelection = ReactiveDatabaseSelectionProvider + .createStaticDatabaseSelectionProvider(databaseName); + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver, databaseSelection); + Mono singleResult = client.delegateTo(runner -> Mono.just(21)).run(); + + StepVerifier.create(singleResult).expectNext(21).verifyComplete(); + + verifyDatabaseSelection("aDatabase"); + + verify(this.session).close(); + } + + @Test + @DisplayName("Queries that return nothing should fit in") + void queriesWithoutResultShouldFitInAsWell() { + + prepareMocks(); + + given(this.session.run(anyString(), anyMap())).willReturn(Mono.just(this.result)); + given(this.result.consume()).willReturn(Mono.just(this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(this.driver); + + String cypher = "DETACH DELETE (b) WHERE name = $name"; + + Mono deletionResult = client.query(cypher).bind("fixie").to("name").run(); + + StepVerifier.create(deletionResult).expectNext(this.resultSummary).verifyComplete(); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "fixie"); + + verify(this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(this.result).consume(); + verify(this.resultSummary).notifications(); + verify(this.resultSummary).hasPlan(); + verify(this.session).close(); + } + + void verifyDatabaseSelection(String targetDatabase) { + + verify(this.driver).session(eq(ReactiveSession.class), this.configArgumentCaptor.capture()); + SessionConfig config = this.configArgumentCaptor.getValue(); + + if (targetDatabase != null) { + assertThat(config.database()).isPresent().contains(targetDatabase); + } + else { + assertThat(config.database()).isEmpty(); + } + } + + void verifyUserSelection(String aUser) { + + verify(this.driver).session(eq(ReactiveSession.class), this.configArgumentCaptor.capture()); + SessionConfig config = this.configArgumentCaptor.getValue(); + + // We assume the driver supports this before the test + final Method impersonatedUser = ReflectionUtils.findMethod(SessionConfig.class, "impersonatedUser"); + if (aUser != null) { + Optional optionalValue = (Optional) ReflectionUtils.invokeMethod(impersonatedUser, config); + assertThat(optionalValue).isPresent().contains(aUser); + } + else { + assertThat(config.database()).isEmpty(); + } + } + + @Nested + @DisplayName("Callback handling should feel good") + class CallbackHandlingShouldFeelGood { + + @Test + void withDefaultDatabase() { + + prepareMocks(); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + Mono singleResult = client.delegateTo(runner -> Mono.just(21)).run(); + + StepVerifier.create(singleResult).expectNext(21).verifyComplete(); + + verifyDatabaseSelection(null); + + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + @Test + void withDatabase() { + + prepareMocks(); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + Mono singleResult = client.delegateTo(runner -> Mono.just(21)).in("aDatabase").run(); + + StepVerifier.create(singleResult).expectNext(21).verifyComplete(); + + verifyDatabaseSelection("aDatabase"); + + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + } + + @Nested + @DisplayName("Mapping should feel good") + class MappingShouldFeelGood { + + @Test + void reading() { + + prepareMocks(); + + given(ReactiveNeo4jClientTests.this.session.run(anyString(), anyMap())) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.result)); + given(ReactiveNeo4jClientTests.this.result.records()) + .willReturn(Flux.just(ReactiveNeo4jClientTests.this.record1)); + given(ReactiveNeo4jClientTests.this.result.consume()) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.resultSummary)); + given(ReactiveNeo4jClientTests.this.record1.get("name")).willReturn(Values.value("michael")); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) RETURN o, collect(b) as bikes"; + + Neo4jClientTests.BikeOwnerReader mappingFunction = new Neo4jClientTests.BikeOwnerReader(); + Flux bikeOwners = client.query(cypher) + .bind("michael") + .to("name") + .fetchAs(Neo4jClientTests.BikeOwner.class) + .mappedBy(mappingFunction) + .all(); + + StepVerifier.create(bikeOwners).expectNextMatches(o -> o.getName().equals("michael")).verifyComplete(); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "michael"); + + verify(ReactiveNeo4jClientTests.this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(ReactiveNeo4jClientTests.this.result).records(); + verify(ReactiveNeo4jClientTests.this.resultSummary).notifications(); + verify(ReactiveNeo4jClientTests.this.resultSummary).hasPlan(); + verify(ReactiveNeo4jClientTests.this.record1).get("name"); + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + @Test + void shouldApplyNullChecksDuringReading() { + + prepareMocks(); + + given(ReactiveNeo4jClientTests.this.session.run(anyString(), anyMap())) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.result)); + given(ReactiveNeo4jClientTests.this.result.records()) + .willReturn(Flux.just(ReactiveNeo4jClientTests.this.record1, ReactiveNeo4jClientTests.this.record2)); + given(ReactiveNeo4jClientTests.this.result.consume()) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.resultSummary)); + given(ReactiveNeo4jClientTests.this.record1.get("name")).willReturn(Values.value("michael")); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + Flux bikeOwners = client.query("MATCH (n) RETURN n") + .fetchAs(Neo4jClientTests.BikeOwner.class) + .mappedBy((t, r) -> { + if (r == ReactiveNeo4jClientTests.this.record1) { + return new Neo4jClientTests.BikeOwner(r.get("name").asString(), Collections.emptyList()); + } + else { + return null; + } + }) + .all(); + + StepVerifier.create(bikeOwners).expectNextCount(1).verifyComplete(); + + verifyDatabaseSelection(null); + + verify(ReactiveNeo4jClientTests.this.session).run(eq("MATCH (n) RETURN n"), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(Collections.emptyMap()))); + verify(ReactiveNeo4jClientTests.this.result).records(); + verify(ReactiveNeo4jClientTests.this.resultSummary).notifications(); + verify(ReactiveNeo4jClientTests.this.resultSummary).hasPlan(); + verify(ReactiveNeo4jClientTests.this.record1).get("name"); + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + @Test + void writing() { + + prepareMocks(); + + given(ReactiveNeo4jClientTests.this.session.run(anyString(), anyMap())) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.result)); + given(ReactiveNeo4jClientTests.this.result.consume()) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + + Neo4jClientTests.BikeOwner michael = new Neo4jClientTests.BikeOwner("Michael", + Arrays.asList(new Neo4jClientTests.Bike("Road"), new Neo4jClientTests.Bike("MTB"))); + String cypher = "MERGE (u:User {name: 'Michael'}) WITH u UNWIND $bikes as bike MERGE (b:Bike {name: bike}) MERGE (u) - [o:OWNS] -> (b) "; + + Mono summary = client.query(cypher) + .bind(michael) + .with(new Neo4jClientTests.BikeOwnerBinder()) + .run(); + + StepVerifier.create(summary).expectNext(ReactiveNeo4jClientTests.this.resultSummary).verifyComplete(); + + verifyDatabaseSelection(null); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Michael"); + + verify(ReactiveNeo4jClientTests.this.session).run(eq(cypher), + MockitoHamcrest.argThat(new Neo4jClientTests.MapAssertionMatcher(expectedParameters))); + verify(ReactiveNeo4jClientTests.this.result).consume(); + verify(ReactiveNeo4jClientTests.this.resultSummary).notifications(); + verify(ReactiveNeo4jClientTests.this.resultSummary).hasPlan(); + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + @Test + @DisplayName("Some automatic conversion is ok") + void automaticConversion() { + + prepareMocks(); + + given(ReactiveNeo4jClientTests.this.session.run(anyString(), anyMap())) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.result)); + given(ReactiveNeo4jClientTests.this.result.records()) + .willReturn(Flux.just(ReactiveNeo4jClientTests.this.record1)); + given(ReactiveNeo4jClientTests.this.result.consume()) + .willReturn(Mono.just(ReactiveNeo4jClientTests.this.resultSummary)); + given(ReactiveNeo4jClientTests.this.record1.size()).willReturn(1); + given(ReactiveNeo4jClientTests.this.record1.get(0)).willReturn(Values.value(23L)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(ReactiveNeo4jClientTests.this.driver); + + String cypher = "MATCH (b:Bike) RETURN count(b)"; + Mono numberOfBikes = client.query(cypher).fetchAs(Long.class).one(); + + StepVerifier.create(numberOfBikes).expectNext(23L).verifyComplete(); + + verifyDatabaseSelection(null); + + verify(ReactiveNeo4jClientTests.this.result).consume(); + verify(ReactiveNeo4jClientTests.this.resultSummary).notifications(); + verify(ReactiveNeo4jClientTests.this.resultSummary).hasPlan(); + verify(ReactiveNeo4jClientTests.this.session).run(eq(cypher), anyMap()); + verify(ReactiveNeo4jClientTests.this.session).close(); + } + + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/core/ResultSummariesTest.java b/src/test/java/org/springframework/data/neo4j/core/ResultSummariesTests.java similarity index 55% rename from src/test/java/org/springframework/data/neo4j/core/ResultSummariesTest.java rename to src/test/java/org/springframework/data/neo4j/core/ResultSummariesTests.java index d60bd0c3b..b92ca4b6a 100644 --- a/src/test/java/org/springframework/data/neo4j/core/ResultSummariesTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/ResultSummariesTests.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; @@ -27,37 +23,35 @@ import org.junit.jupiter.params.provider.MethodSource; import org.neo4j.driver.summary.InputPosition; import org.neo4j.driver.summary.Notification; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + /** * @author Michael J. Simons * @author Kevin Wittek - * @soundtrack Fatoni & Dexter - Yo, Picasso */ -class ResultSummariesTest { +class ResultSummariesTests { private static final String LINE_SEPARATOR = System.lineSeparator(); private static Stream params() { return Stream.of( - Arguments.of("match (n) - [r:FOO*] -> (m) RETURN r", 1, 19, "" - + "\tmatch (n) - [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR - + "\t ^" + LINE_SEPARATOR), - Arguments.of("match (n)\n- [r:FOO*] -> (m) RETURN r", 2, 1, "" - + "\tmatch (n)" + LINE_SEPARATOR - + "\t- [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR - + "\t^" + LINE_SEPARATOR), - Arguments.of("match (x0123456789) \nwith x0123456789\nmatch(n) - [r:FOO*] -> (m) RETURN r", 3, 10, "" - + "\tmatch (x0123456789) " + LINE_SEPARATOR - + "\twith x0123456789" + LINE_SEPARATOR - + "\tmatch(n) - [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR - + "\t ^" + LINE_SEPARATOR), - Arguments.of("match (n) \n- [r:FOO*] -> (m) \nRETURN r", 2, 1, "" - + "\tmatch (n) " + LINE_SEPARATOR - + "\t- [r:FOO*] -> (m) " + LINE_SEPARATOR - + "\t^" + LINE_SEPARATOR - + "\tRETURN r" + LINE_SEPARATOR), - Arguments.of("match (n) - [r] -> (m) RETURN r", null, null, "" - + "\tmatch (n) - [r] -> (m) RETURN r" + LINE_SEPARATOR) - ); + Arguments.of("match (n) - [r:FOO*] -> (m) RETURN r", 1, 19, + "\tmatch (n) - [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR + "\t ^" + + LINE_SEPARATOR), + Arguments.of("match (n)\n- [r:FOO*] -> (m) RETURN r", 2, 1, + "\tmatch (n)" + LINE_SEPARATOR + "\t- [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR + "\t^" + + LINE_SEPARATOR), + Arguments.of("match (x0123456789) \nwith x0123456789\nmatch(n) - [r:FOO*] -> (m) RETURN r", 3, 10, + "\tmatch (x0123456789) " + LINE_SEPARATOR + "\twith x0123456789" + LINE_SEPARATOR + + "\tmatch(n) - [r:FOO*] -> (m) RETURN r" + LINE_SEPARATOR + "\t ^" + + LINE_SEPARATOR), + Arguments.of("match (n) \n- [r:FOO*] -> (m) \nRETURN r", 2, 1, + "\tmatch (n) " + LINE_SEPARATOR + "\t- [r:FOO*] -> (m) " + + LINE_SEPARATOR + "\t^" + LINE_SEPARATOR + "\tRETURN r" + LINE_SEPARATOR), + Arguments.of("match (n) - [r] -> (m) RETURN r", null, null, + "\tmatch (n) - [r] -> (m) RETURN r" + LINE_SEPARATOR)); } @ParameterizedTest(name = "{index}: Notifications for \"{0}\"") @@ -67,22 +61,23 @@ class ResultSummariesTest { InputPosition inputPosition; if (line == null || column == null) { inputPosition = null; - } else { + } + else { inputPosition = mock(InputPosition.class); - when(inputPosition.line()).thenReturn(line); - when(inputPosition.column()).thenReturn(column); + given(inputPosition.line()).willReturn(line); + given(inputPosition.column()).willReturn(column); } Notification notification = mock(Notification.class); - when(notification.severity()).thenReturn("WARNING"); - when(notification.code()).thenReturn("KGQ.Warning"); - when(notification.title()).thenReturn("Das ist keine gute Query."); - when(notification.description()).thenReturn("Das solltest Du besser nicht mehr machen."); - when(notification.position()).thenReturn(inputPosition); + given(notification.severity()).willReturn("WARNING"); + given(notification.code()).willReturn("KGQ.Warning"); + given(notification.title()).willReturn("Das ist keine gute Query."); + given(notification.description()).willReturn("Das solltest Du besser nicht mehr machen."); + given(notification.position()).willReturn(inputPosition); String formattedNotification = ResultSummaries.format(notification, query); - assertThat(formattedNotification).isEqualTo("KGQ.Warning: Das ist keine gute Query." + LINE_SEPARATOR - + expected + assertThat(formattedNotification).isEqualTo("KGQ.Warning: Das ist keine gute Query." + LINE_SEPARATOR + expected + "Das solltest Du besser nicht mehr machen."); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java b/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTests.java similarity index 59% rename from src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java rename to src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTests.java index b41034414..528a9e12d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTests.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.when; - import java.time.LocalDate; import java.time.Period; @@ -31,17 +26,23 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.Record; import org.neo4j.driver.Values; import org.neo4j.driver.types.TypeSystem; + import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; + /** * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class SingleValueMappingFunctionTest { +class SingleValueMappingFunctionTests { private final TypeSystem typeSystem; @@ -49,7 +50,7 @@ class SingleValueMappingFunctionTest { private final ConversionService conversionService; - SingleValueMappingFunctionTest(@Mock TypeSystem typeSystem, @Mock Record record) { + SingleValueMappingFunctionTests(@Mock TypeSystem typeSystem, @Mock Record record) { this.typeSystem = typeSystem; this.record = record; this.conversionService = new DefaultConversionService(); @@ -57,68 +58,74 @@ class SingleValueMappingFunctionTest { } + @Test + void shouldWorkWithNullValues() { + + given(this.record.size()).willReturn(1); + given(this.record.get(0)).willReturn(Values.NULL); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(this.conversionService, + String.class); + assertThat(mappingFunction.apply(this.typeSystem, this.record)).isNull(); + } + + @Test + void shouldCheckReturnType() { + + given(this.record.size()).willReturn(1); + given(this.record.get(0)).willReturn(Values.value("Guten Tag.")); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(this.conversionService, + Period.class); + + assertThatExceptionOfType(ConversionFailedException.class) + .isThrownBy(() -> mappingFunction.apply(this.typeSystem, this.record)) + .withMessageContainingAll("Failed to convert from type", "org.neo4j.driver.internal.value.StringValue", + "to type [java.time.Period] for value"); + } + + @Test + void mappingShouldWorkForSupportedTypes() { + + LocalDate aDate = LocalDate.of(2019, 4, 10); + + given(this.record.size()).willReturn(1); + given(this.record.get(0)).willReturn(Values.value(aDate)); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(this.conversionService, + LocalDate.class); + assertThat(mappingFunction.apply(this.typeSystem, this.record)).isEqualTo(aDate); + } + @Nested class ShouldCheckForRecordSize { @Test void shouldNotMapNothing() { - when(record.size()).thenReturn(0); + given(SingleValueMappingFunctionTests.this.record.size()).willReturn(0); - SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(conversionService, - String.class); - assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record)) - .withMessage("Record has no elements, cannot map nothing"); + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>( + SingleValueMappingFunctionTests.this.conversionService, String.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> mappingFunction.apply(SingleValueMappingFunctionTests.this.typeSystem, + SingleValueMappingFunctionTests.this.record)) + .withMessage("Record has no elements, cannot map nothing"); } @Test void shouldNotMapAmbiguousThings() { - when(record.size()).thenReturn(23); + given(SingleValueMappingFunctionTests.this.record.size()).willReturn(23); - SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(conversionService, - String.class); - assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record)) - .withMessage("Records with more than one value cannot be converted without a mapper"); + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>( + SingleValueMappingFunctionTests.this.conversionService, String.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> mappingFunction.apply(SingleValueMappingFunctionTests.this.typeSystem, + SingleValueMappingFunctionTests.this.record)) + .withMessage("Records with more than one value cannot be converted without a mapper"); } + } - @Test - void shouldWorkWithNullValues() { - - when(record.size()).thenReturn(1); - when(record.get(0)).thenReturn(Values.NULL); - - SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(conversionService, - String.class); - assertThat(mappingFunction.apply(typeSystem, record)).isNull(); - } - - @Test - void shouldCheckReturnType() { - - when(record.size()).thenReturn(1); - when(record.get(0)).thenReturn(Values.value("Guten Tag.")); - - SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(conversionService, - Period.class); - - assertThatExceptionOfType(ConversionFailedException.class) - .isThrownBy(() -> mappingFunction.apply(typeSystem, record)).withMessageContainingAll( - "Failed to convert from type", "org.neo4j.driver.internal.value.StringValue", - "to type [java.time.Period] for value"); - } - - @Test - void mappingShouldWorkForSupportedTypes() { - - LocalDate aDate = LocalDate.of(2019, 4, 10); - - when(record.size()).thenReturn(1); - when(record.get(0)).thenReturn(Values.value(aDate)); - - SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(conversionService, - LocalDate.class); - assertThat(mappingFunction.apply(typeSystem, record)).isEqualTo(aDate); - } } diff --git a/src/test/java/org/springframework/data/neo4j/core/TemplateSupportTest.java b/src/test/java/org/springframework/data/neo4j/core/TemplateSupportTests.java similarity index 98% rename from src/test/java/org/springframework/data/neo4j/core/TemplateSupportTest.java rename to src/test/java/org/springframework/data/neo4j/core/TemplateSupportTests.java index 9659855f3..bc07e5d29 100644 --- a/src/test/java/org/springframework/data/neo4j/core/TemplateSupportTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/TemplateSupportTests.java @@ -15,53 +15,17 @@ */ package org.springframework.data.neo4j.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class TemplateSupportTest { - - static class A { - } - - static class B { - } - - static class A2 extends A { - } - - static class A3 extends A { - } - - static class A4 extends A { - } - - static class AA2 extends A2 { - } - - interface IA { - } - - interface IB { - } - - static class B1 implements IA { - } - - static class B2 implements IA { - } - - static class B3 implements IA, IB { - } - - static class B4 implements IA, IB { - } +class TemplateSupportTests { @Test void shouldFindCommonElementTypeOfHeterousCollection() { @@ -134,4 +98,53 @@ class TemplateSupportTest { type = TemplateSupport.findCommonElementType(Arrays.asList(new B(), new A(), new A())); assertThat(type).isNull(); } + + interface IA { + + } + + interface IB { + + } + + static class A { + + } + + static class B { + + } + + static class A2 extends A { + + } + + static class A3 extends A { + + } + + static class A4 extends A { + + } + + static class AA2 extends A2 { + + } + + static class B1 implements IA { + + } + + static class B2 implements IA { + + } + + static class B3 implements IA, IB { + + } + + static class B4 implements IA, IB { + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java deleted file mode 100644 index eb08e0e5d..000000000 --- a/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2011-2025 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.core; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.Driver; -import org.neo4j.driver.QueryRunner; -import org.neo4j.driver.Session; -import org.neo4j.driver.SessionConfig; -import org.neo4j.driver.Transaction; -import org.neo4j.driver.TransactionConfig; -import org.neo4j.driver.reactivestreams.ReactiveSession; -import org.neo4j.driver.reactivestreams.ReactiveTransaction; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; - -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -/** - * Ensure correct behaviour of both imperative and reactive clients in and outside Springs transaction management. - * - * @author Michael J. Simons - */ -@ExtendWith(MockitoExtension.class) -class TransactionHandlingTest { - - @Mock private Driver driver; - - @Mock private Session session; - - @Nested - class Neo4jClientTest { - - @Mock private Transaction transaction; - - @Nested - class AutoCloseableQueryRunnerHandlerTest { - - @Test - void shouldCallCloseOnSession() { - - ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); - - when(driver.session(any(SessionConfig.class))).thenReturn(session); - - // Make template acquire session - DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(Neo4jClient.with(driver)); - try (QueryRunner s = neo4jClient.getQueryRunner(DatabaseSelection.byName("aDatabase"))) { - s.run("MATCH (n) RETURN n"); - } catch (Exception e) { - throw new RuntimeException(e); - } - - verify(driver).session(configArgumentCaptor.capture()); - SessionConfig sessionConfig = configArgumentCaptor.getValue(); - assertThat(sessionConfig.database()).isPresent().contains("aDatabase"); - - verify(session).run(any(String.class)); - verify(session).lastBookmarks(); - verify(session).close(); - - verifyNoMoreInteractions(driver, session, transaction); - } - - @Test - void shouldNotInvokeCloseOnTransaction() { - - AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - - when(driver.session(any(SessionConfig.class))).thenReturn(session); - when(session.isOpen()).thenReturn(true); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); - // Mock closing of the transaction - doAnswer(invocation -> { - transactionIsOpen.set(false); - return null; - }).when(transaction).close(); - when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); - - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); - - DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(Neo4jClient.with(driver)); - txTemplate.execute(tx -> { - try (QueryRunner s = neo4jClient.getQueryRunner(DatabaseSelection.undecided())) { - s.run("MATCH (n) RETURN n"); - } catch (Exception e) { - throw new RuntimeException(e); - } - return null; - }); - - verify(transaction, times(2)).isOpen(); - verify(transaction).run(anyString()); - // Called by the transaction manager - verify(transaction).commit(); - verify(transaction).close(); - verify(session).isOpen(); - verify(session).lastBookmarks(); - verify(session).close(); - verifyNoMoreInteractions(driver, session, transaction); - } - } - } - - @Nested - class ReactiveNeo4jClientTest { - - @Mock private ReactiveSession session; - - @Mock private ReactiveTransaction transaction; - - @Test - void shouldNotOpenTransactionsWithoutSubscription() { - DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(ReactiveNeo4jClient.with(driver)); - neo4jClient.query("RETURN 1").in("aDatabase").fetch().one(); - - verify(driver, never()).session(eq(ReactiveSession.class), any(SessionConfig.class)); - verifyNoMoreInteractions(driver, session); - } - - @Test - void shouldCloseUnmanagedSessionOnComplete() { - - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).thenReturn(session); - when(session.close()).thenReturn(Mono.empty()); - - DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(ReactiveNeo4jClient.with(driver)); - - Mono sequence = neo4jClient.doInQueryRunnerForMono(Mono.just(DatabaseSelection.byName("aDatabase")), Mono.just(UserSelection.connectedUser()), tx -> Mono.just("1")); - - StepVerifier.create(sequence).expectNext("1").verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - verify(session).lastBookmarks(); - verify(session).close(); - verifyNoMoreInteractions(driver, session, transaction); - } - - @Test - void shouldCloseUnmanagedSessionOnError() { - - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).thenReturn(session); - when(session.close()).thenReturn(Mono.empty()); - - DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(ReactiveNeo4jClient.with(driver)); - - Mono sequence = neo4jClient.doInQueryRunnerForMono(Mono.just(DatabaseSelection.byName("aDatabase")), Mono.just(UserSelection.connectedUser()), tx -> Mono.error(new SomeException())); - - StepVerifier.create(sequence).expectError(SomeException.class).verify(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - verify(session).lastBookmarks(); - verify(session).close(); - verifyNoMoreInteractions(driver, session, transaction); - } - } - - private static class SomeException extends RuntimeException {} -} diff --git a/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTests.java b/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTests.java new file mode 100644 index 000000000..362ef6eac --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2011-2025 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.core; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.Driver; +import org.neo4j.driver.QueryRunner; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.reactivestreams.ReactiveSession; +import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Ensure correct behaviour of both imperative and reactive clients in and outside Springs + * transaction management. + * + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class TransactionHandlingTests { + + @Mock + private Driver driver; + + @Mock + private Session session; + + private static final class SomeException extends RuntimeException { + + } + + @Nested + class Neo4jClientTest { + + @Mock + private Transaction transaction; + + @Nested + class AutoCloseableQueryRunnerHandlerTest { + + @Test + void shouldCallCloseOnSession() { + + ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); + + given(TransactionHandlingTests.this.driver.session(any(SessionConfig.class))) + .willReturn(TransactionHandlingTests.this.session); + + // Make template acquire session + DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient( + Neo4jClient.with(TransactionHandlingTests.this.driver)); + try (QueryRunner s = neo4jClient.getQueryRunner(DatabaseSelection.byName("aDatabase"))) { + s.run("MATCH (n) RETURN n"); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + + verify(TransactionHandlingTests.this.driver).session(configArgumentCaptor.capture()); + SessionConfig sessionConfig = configArgumentCaptor.getValue(); + assertThat(sessionConfig.database()).isPresent().contains("aDatabase"); + + verify(TransactionHandlingTests.this.session).run(any(String.class)); + verify(TransactionHandlingTests.this.session).lastBookmarks(); + verify(TransactionHandlingTests.this.session).close(); + + verifyNoMoreInteractions(TransactionHandlingTests.this.driver, TransactionHandlingTests.this.session, + Neo4jClientTest.this.transaction); + } + + @Test + void shouldNotInvokeCloseOnTransaction() { + + AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + + given(TransactionHandlingTests.this.driver.session(any(SessionConfig.class))) + .willReturn(TransactionHandlingTests.this.session); + given(TransactionHandlingTests.this.session.isOpen()).willReturn(true); + given(TransactionHandlingTests.this.session.beginTransaction(any(TransactionConfig.class))) + .willReturn(Neo4jClientTest.this.transaction); + // Mock closing of the transaction + BDDMockito.doAnswer(invocation -> { + transactionIsOpen.set(false); + return null; + }).when(Neo4jClientTest.this.transaction).close(); + given(Neo4jClientTest.this.transaction.isOpen()).willAnswer(invocation -> transactionIsOpen.get()); + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(TransactionHandlingTests.this.driver); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient( + Neo4jClient.with(TransactionHandlingTests.this.driver)); + txTemplate.execute(tx -> { + try (QueryRunner s = neo4jClient.getQueryRunner(DatabaseSelection.undecided())) { + s.run("MATCH (n) RETURN n"); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + return null; + }); + + verify(Neo4jClientTest.this.transaction, times(2)).isOpen(); + verify(Neo4jClientTest.this.transaction).run(anyString()); + // Called by the transaction manager + verify(Neo4jClientTest.this.transaction).commit(); + verify(Neo4jClientTest.this.transaction).close(); + verify(TransactionHandlingTests.this.session).isOpen(); + verify(TransactionHandlingTests.this.session).lastBookmarks(); + verify(TransactionHandlingTests.this.session).close(); + verifyNoMoreInteractions(TransactionHandlingTests.this.driver, TransactionHandlingTests.this.session, + Neo4jClientTest.this.transaction); + } + + } + + } + + @Nested + class ReactiveNeo4jClientTest { + + @Mock + private ReactiveSession session; + + @Mock + private ReactiveTransaction transaction; + + @Test + void shouldNotOpenTransactionsWithoutSubscription() { + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient( + ReactiveNeo4jClient.with(TransactionHandlingTests.this.driver)); + neo4jClient.query("RETURN 1").in("aDatabase").fetch().one(); + + verify(TransactionHandlingTests.this.driver, never()).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + verifyNoMoreInteractions(TransactionHandlingTests.this.driver, this.session); + } + + @Test + void shouldCloseUnmanagedSessionOnComplete() { + + given(TransactionHandlingTests.this.driver.session(eq(ReactiveSession.class), any(SessionConfig.class))) + .willReturn(this.session); + given(this.session.close()).willReturn(Mono.empty()); + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient( + ReactiveNeo4jClient.with(TransactionHandlingTests.this.driver)); + + Mono sequence = neo4jClient.doInQueryRunnerForMono(Mono.just(DatabaseSelection.byName("aDatabase")), + Mono.just(UserSelection.connectedUser()), tx -> Mono.just("1")); + + StepVerifier.create(sequence).expectNext("1").verifyComplete(); + + verify(TransactionHandlingTests.this.driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); + verify(this.session).lastBookmarks(); + verify(this.session).close(); + verifyNoMoreInteractions(TransactionHandlingTests.this.driver, this.session, this.transaction); + } + + @Test + void shouldCloseUnmanagedSessionOnError() { + + given(TransactionHandlingTests.this.driver.session(eq(ReactiveSession.class), any(SessionConfig.class))) + .willReturn(this.session); + given(this.session.close()).willReturn(Mono.empty()); + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient( + ReactiveNeo4jClient.with(TransactionHandlingTests.this.driver)); + + Mono sequence = neo4jClient.doInQueryRunnerForMono(Mono.just(DatabaseSelection.byName("aDatabase")), + Mono.just(UserSelection.connectedUser()), tx -> Mono.error(new SomeException())); + + StepVerifier.create(sequence).expectError(SomeException.class).verify(); + + verify(TransactionHandlingTests.this.driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); + verify(this.session).lastBookmarks(); + verify(this.session).close(); + verifyNoMoreInteractions(TransactionHandlingTests.this.driver, this.session, this.transaction); + } + + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTest.java b/src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTests.java similarity index 98% rename from src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTest.java rename to src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTests.java index b2bd51e09..4608c3a62 100644 --- a/src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/convert/SpatialTypesTests.java @@ -15,19 +15,20 @@ */ package org.springframework.data.neo4j.core.convert; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.neo4j.driver.types.Point; + import org.springframework.data.neo4j.types.CartesianPoint2d; import org.springframework.data.neo4j.types.CartesianPoint3d; import org.springframework.data.neo4j.types.GeographicPoint2d; import org.springframework.data.neo4j.types.GeographicPoint3d; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class SpatialTypesTest { +class SpatialTypesTests { @Test void neo4jPointAsValueShouldWork() { @@ -56,4 +57,5 @@ class SpatialTypesTest { assertThat(point.y()).isEqualTo(20.0); assertThat(point.z()).isEqualTo(30.0); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTest.java b/src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTests.java similarity index 52% rename from src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTest.java rename to src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTests.java index 642b12f7b..9c92a113d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/convert/TemporalAmountAdapterTests.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.convert; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.Duration; import java.time.LocalDate; import java.time.Period; @@ -26,45 +24,52 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Values; import org.neo4j.driver.types.IsoDuration; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class TemporalAmountAdapterTest { +class TemporalAmountAdapterTests { private final TemporalAmountAdapter underTest = new TemporalAmountAdapter(); @Test - public void internallyCreatedTypesShouldBeConvertedCorrect() { + void internallyCreatedTypesShouldBeConvertedCorrect() { - assertThat(underTest.apply(Values.isoDuration(1, 0, 0, 0).asIsoDuration())).isEqualTo(Period.ofMonths(1)); - assertThat(underTest.apply(Values.isoDuration(1, 1, 0, 0).asIsoDuration())).isEqualTo(Period.ofMonths(1).plusDays(1)); - assertThat(underTest.apply(Values.isoDuration(1, 1, 1, 0).asIsoDuration())) - .isEqualTo(Values.isoDuration(1, 1, 1, 0).asIsoDuration()); - assertThat(underTest.apply(Values.isoDuration(0, 0, 120, 1).asIsoDuration())) - .isEqualTo(Duration.ofMinutes(2).plusNanos(1)); + assertThat(this.underTest.apply(Values.isoDuration(1, 0, 0, 0).asIsoDuration())).isEqualTo(Period.ofMonths(1)); + assertThat(this.underTest.apply(Values.isoDuration(1, 1, 0, 0).asIsoDuration())) + .isEqualTo(Period.ofMonths(1).plusDays(1)); + assertThat(this.underTest.apply(Values.isoDuration(1, 1, 1, 0).asIsoDuration())) + .isEqualTo(Values.isoDuration(1, 1, 1, 0).asIsoDuration()); + assertThat(this.underTest.apply(Values.isoDuration(0, 0, 120, 1).asIsoDuration())) + .isEqualTo(Duration.ofMinutes(2).plusNanos(1)); } @Test - public void durationsShouldStayDurations() { + void durationsShouldStayDurations() { - Duration duration = ChronoUnit.MONTHS.getDuration().multipliedBy(13).plus(ChronoUnit.DAYS.getDuration().multipliedBy(32)).plusHours(25) - .plusMinutes(120); + Duration duration = ChronoUnit.MONTHS.getDuration() + .multipliedBy(13) + .plus(ChronoUnit.DAYS.getDuration().multipliedBy(32)) + .plusHours(25) + .plusMinutes(120); - assertThat(underTest.apply(Values.value(duration).asIsoDuration())).isEqualTo(duration); + assertThat(this.underTest.apply(Values.value(duration).asIsoDuration())).isEqualTo(duration); } @Test - public void periodsShouldStayPeriods() { + void periodsShouldStayPeriods() { Period period = Period.between(LocalDate.of(2018, 11, 15), LocalDate.of(2020, 12, 24)); - assertThat(underTest.apply(Values.value(period).asIsoDuration())).isEqualTo(period.normalized()); + assertThat(this.underTest.apply(Values.value(period).asIsoDuration())).isEqualTo(period.normalized()); } @Test // GH-2324 - public void zeroDurationShouldReturnTheIsoDuration() { + void zeroDurationShouldReturnTheIsoDuration() { IsoDuration zeroDuration = Values.isoDuration(0, 0, 0, 0).asIsoDuration(); - assertThat(underTest.apply(zeroDuration)).isSameAs(zeroDuration); + assertThat(this.underTest.apply(zeroDuration)).isSameAs(zeroDuration); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTests.java similarity index 64% rename from src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTests.java index 4c31a5eff..87e4104bf 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTests.java @@ -15,18 +15,12 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; - import java.util.Collection; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -38,45 +32,62 @@ import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Dialect; import org.neo4j.cypherdsl.core.renderer.Renderer; + import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; + /** * @author Davide Fantuzzi * @author Andrea Santurbano * @author Michael J. Simons */ -class CypherGeneratorTest { +class CypherGeneratorTests { + + private static Stream pageables() { + return Stream.of( + Arguments.of(Sort.by("a", "b").and(Sort.by(Sort.Order.asc("foo"), Sort.Order.desc("bar"))), + Optional.of("ORDER BY a ASC, b ASC, foo ASC, bar DESC")), + Arguments.of(null, Optional.empty()), Arguments.of(Sort.unsorted(), Optional.empty()), + Arguments.of(Sort.by("n.a").ascending(), Optional.of("ORDER BY n.a ASC"))); + } @Test void shouldCreateRelationshipCreationQueryWithLabelIfPresent() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext().getPersistentEntity(Entity1.class); RelationshipDescription relationshipDescription = Mockito.mock(RelationshipDescription.class); - when(relationshipDescription.isDynamic()).thenReturn(true); + given(relationshipDescription.isDynamic()).willReturn(true); Statement statement = CypherGenerator.INSTANCE.prepareSaveOfRelationship(persistentEntity, relationshipDescription, "REL", true); String expectedQuery = "MATCH (startNode:`Entity1`) WHERE startNode.id = $fromId MATCH (endNode)" - + " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); + + " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__"; + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @Test void shouldCreateRelationshipCreationQueryWithMultipleLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(MultipleLabelEntity1.class); + .getPersistentEntity(MultipleLabelEntity1.class); RelationshipDescription relationshipDescription = Mockito.mock(RelationshipDescription.class); - when(relationshipDescription.isDynamic()).thenReturn(true); + given(relationshipDescription.isDynamic()).willReturn(true); Statement statement = CypherGenerator.INSTANCE.prepareSaveOfRelationship(persistentEntity, relationshipDescription, "REL", true); - String expectedQuery = - "MATCH (startNode:`Entity1`:`MultipleLabel`) WHERE startNode.id = $fromId MATCH (endNode)" + String expectedQuery = "MATCH (startNode:`Entity1`:`MultipleLabel`) WHERE startNode.id = $fromId MATCH (endNode)" + " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @Test @@ -86,17 +97,19 @@ class CypherGeneratorTest { Neo4jPersistentProperty persistentProperty = Mockito.mock(Neo4jPersistentProperty.class); doReturn(Long.class).when(persistentProperty).getType(); - when(relationshipDescription.isDynamic()).thenReturn(true); - when(persistentEntity.isUsingInternalIds()).thenReturn(true); - when(persistentEntity.getRequiredIdProperty()).thenReturn(persistentProperty); - when(persistentEntity.isUsingDeprecatedInternalId()).thenReturn(true); + given(relationshipDescription.isDynamic()).willReturn(true); + given(persistentEntity.isUsingInternalIds()).willReturn(true); + given(persistentEntity.getRequiredIdProperty()).willReturn(persistentProperty); + given(persistentEntity.isUsingDeprecatedInternalId()).willReturn(true); Statement statement = CypherGenerator.INSTANCE.prepareSaveOfRelationship(persistentEntity, relationshipDescription, "REL", true); String expectedQuery = "MATCH (startNode) WHERE id(startNode) = $fromId MATCH (endNode)" - + " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); + + " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__"; + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @Test @@ -109,22 +122,27 @@ class CypherGeneratorTest { Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription, true); String expectedQuery = "MATCH (startNode:`Entity1`)<-[rel]-(:`Entity2`) WHERE (startNode.id = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @Test void shouldCreateRelationshipRemoveQueryWithMultipleLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(MultipleLabelEntity1.class); + .getPersistentEntity(MultipleLabelEntity1.class); Neo4jPersistentEntity relatedEntity = new Neo4jMappingContext() - .getPersistentEntity(MultipleLabelEntity2.class); + .getPersistentEntity(MultipleLabelEntity2.class); RelationshipDescription relationshipDescription = Mockito.mock(RelationshipDescription.class); doReturn(relatedEntity).when(relationshipDescription).getTarget(); Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription, true); String expectedQuery = "MATCH (startNode:`Entity1`:`MultipleLabel`)<-[rel]-(:`Entity2`:`MultipleLabel`) WHERE (startNode.id = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); + + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @Test @@ -138,26 +156,17 @@ class CypherGeneratorTest { doReturn(Long.class).when(persistentProperty).getType(); doReturn(relatedEntity).when(relationshipDescription).getTarget(); - when(relationshipDescription.isDynamic()).thenReturn(true); - when(persistentEntity.isUsingInternalIds()).thenReturn(true); - when(persistentEntity.getRequiredIdProperty()).thenReturn(persistentProperty); - when(persistentEntity.isUsingDeprecatedInternalId()).thenReturn(true); + given(relationshipDescription.isDynamic()).willReturn(true); + given(persistentEntity.isUsingInternalIds()).willReturn(true); + given(persistentEntity.getRequiredIdProperty()).willReturn(persistentProperty); + given(persistentEntity.isUsingDeprecatedInternalId()).willReturn(true); Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription, true); String expectedQuery = "MATCH (startNode)<-[rel]-(:`Entity2`) WHERE (id(startNode) = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel"; - Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)); - } - - private static Stream pageables() { - return Stream.of( - Arguments.of(Sort.by("a", "b").and( - Sort.by(Sort.Order.asc("foo"), Sort.Order.desc("bar"))), - Optional.of("ORDER BY a ASC, b ASC, foo ASC, bar DESC")), - Arguments.of(null, Optional.empty()), - Arguments.of(Sort.unsorted(), Optional.empty()), - Arguments.of(Sort.by("n.a").ascending(), Optional.of("ORDER BY n.a ASC")) - ); + assertThat( + Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement)) + .isEqualTo(expectedQuery); } @ParameterizedTest // DATAGRAPH-1440 @@ -171,32 +180,34 @@ class CypherGeneratorTest { @Test void shouldFailOnInvalidPath() { - assertThatIllegalArgumentException().isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n."))) - .withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path"); + assertThatIllegalArgumentException() + .isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n."))) + .withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path"); } @Test void shouldFailOnInvalidPathWithMultipleHops() { - assertThatIllegalArgumentException().isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n.n.n"))) - .withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path"); + assertThatIllegalArgumentException() + .isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n.n.n"))) + .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`"))); + 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", - "apoc.text.clean() |true | ORDER BY apoc.text.clean() DESC", - "date() |false| ORDER BY date() ASC", - "date({year:1984, month:10, day:11})|false| ORDER BY date({year:1984, month:10, day:11}) ASC", - "round(3.141592, 3) |false| ORDER BY round(3.141592, 3) 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", + "apoc.text.clean() |true | ORDER BY apoc.text.clean() DESC", + "date() |false| ORDER BY date() ASC", + "date({year:1984, month:10, day:11})|false| ORDER BY date({year:1984, month:10, day:11}) ASC", + "round(3.141592, 3) |false| ORDER BY round(3.141592, 3) ASC" }) @ParameterizedTest // GH-2273 void functionCallsShouldWork(String input, boolean descending, String expected) { @@ -211,24 +222,27 @@ class CypherGeneratorTest { @Test void shouldFailOnInvalidSymbolicNames() { - assertThatIllegalArgumentException().isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("()"))) - .withMessage("Name must be a valid identifier"); + assertThatIllegalArgumentException() + .isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("()"))) + .withMessage("Name must be a valid identifier"); } @Test void shouldCreateDynamicRelationshipPathQueryForEnumsWithoutWildcardRelationships() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(CyclicEntityWithEnumeratedDynamicRelationship1.class); + .getPersistentEntity(CyclicEntityWithEnumeratedDynamicRelationship1.class); org.neo4j.cypherdsl.core.Node rootNode = Cypher.anyNode(Constants.NAME_OF_ROOT_NODE); Collection relationships = persistentEntity.getRelationships(); - Statement statement = CypherGenerator.INSTANCE.prepareMatchOf( - persistentEntity, relationships.iterator().next(), null, null).returning(rootNode).build(); + Statement statement = CypherGenerator.INSTANCE + .prepareMatchOf(persistentEntity, relationships.iterator().next(), null, null) + .returning(rootNode) + .build(); - // we want to ensure that the pattern occurs three times but do not care about the order + // we want to ensure that the pattern occurs three times but do not care about the + // order // of the relationship types - Pattern relationshipTypesPattern = - Pattern.compile("\\[__sr__:(`CORNERED`\\|`ROUND`|`ROUND`\\|`CORNERED`)]"); + Pattern relationshipTypesPattern = Pattern.compile("\\[__sr__:(`CORNERED`\\|`ROUND`|`ROUND`\\|`CORNERED`)]"); Pattern untypedRelationshipsPattern = Pattern.compile("\\[__sr__]"); @@ -240,96 +254,115 @@ class CypherGeneratorTest { @Test void shouldCreateDynamicRelationshipPathQueryForStringsWithWildcardRelationships() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(CyclicEntityWithStringDynamicRelationship1.class); + .getPersistentEntity(CyclicEntityWithStringDynamicRelationship1.class); org.neo4j.cypherdsl.core.Node rootNode = Cypher.anyNode(Constants.NAME_OF_ROOT_NODE); Collection relationships = persistentEntity.getRelationships(); - Statement statement = CypherGenerator.INSTANCE.prepareMatchOf( - persistentEntity, relationships.iterator().next(), null, null).returning(rootNode).build(); + Statement statement = CypherGenerator.INSTANCE + .prepareMatchOf(persistentEntity, relationships.iterator().next(), null, null) + .returning(rootNode) + .build(); Pattern untypedRelationshipsPattern = Pattern.compile("\\[__sr__]"); - Pattern typedRelationshipsPattern = Pattern.compile("\\[__sr__:(`.*`)]"); + Pattern typedRelationshipsPattern = Pattern.compile("\\[__sr__:(`.*`)]"); String renderedStatement = Renderer.getDefaultRenderer().render(statement); assertThat(renderedStatement).containsPattern(untypedRelationshipsPattern); assertThat(renderedStatement).doesNotContainPattern(typedRelationshipsPattern); } - @Node - private static class Entity1 { + enum CyclicRelationship { - @Id private Long id; + ROUND, CORNERED + + } + + @Node + private static final class Entity1 { + + @Id + private Long id; private String name; private Map dynamicRelationships; + } @Node({ "Entity1", "MultipleLabel" }) - private static class MultipleLabelEntity1 { + private static final class MultipleLabelEntity1 { - @Id private Long id; + @Id + private Long id; private String name; private Map dynamicRelationships; + } @Node - private static class Entity2 { + private static final class Entity2 { - @Id private Long id; + @Id + private Long id; private String name; private Map dynamicRelationships; + } @Node({ "Entity2", "MultipleLabel" }) - private static class MultipleLabelEntity2 { + private static final class MultipleLabelEntity2 { - @Id private Long id; + @Id + private Long id; private String name; private Map dynamicRelationships; - } - enum CyclicRelationship { - ROUND, - CORNERED } @Node - private static class CyclicEntityWithEnumeratedDynamicRelationship1 { + private static final class CyclicEntityWithEnumeratedDynamicRelationship1 { - @Id private Long id; + @Id + private Long id; private Map dynamicRelationship; + } @Node - private static class CyclicEntityWithEnumeratedDynamicRelationship2 { + private static final class CyclicEntityWithEnumeratedDynamicRelationship2 { - @Id private Long id; + @Id + private Long id; private Map dynamicRelationship; + } @Node - private static class CyclicEntityWithStringDynamicRelationship1 { + private static final class CyclicEntityWithStringDynamicRelationship1 { - @Id private Long id; + @Id + private Long id; private Map dynamicRelationship; + } @Node - private static class CyclicEntityWithStringDynamicRelationship2 { + private static final class CyclicEntityWithStringDynamicRelationship2 { - @Id private Long id; + @Id + private Long id; private Map dynamicRelationship; + } } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTests.java similarity index 59% rename from src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTests.java index d7b16d57e..5f5ea75ad 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionServiceTests.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.time.Duration; import java.time.LocalDate; import java.time.Period; @@ -30,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; + import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.dao.TypeMismatchDataAccessException; @@ -37,13 +35,16 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.util.TypeInformation; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Michael J. Simons - * @soundtrack Trettmann, KitschKrieg - Trettmann */ -class DefaultNeo4jConversionServiceTest { +class DefaultNeo4jConversionServiceTests { - private final DefaultNeo4jConversionService defaultNeo4jEntityAccessor = new DefaultNeo4jConversionService(new Neo4jConversions()); + private final DefaultNeo4jConversionService defaultNeo4jEntityAccessor = new DefaultNeo4jConversionService( + new Neo4jConversions()); @Nested class Reads { @@ -52,7 +53,8 @@ class DefaultNeo4jConversionServiceTest { void shouldDealWith0IsoDurationsAsPeriods() { Value zeroDuration = Values.isoDuration(0, 0, 0, 0); - Period period = (Period) defaultNeo4jEntityAccessor.readValue(zeroDuration, TypeInformation.of(Period.class), null); + Period period = (Period) DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor + .readValue(zeroDuration, TypeInformation.of(Period.class), null); assertThat(period.isZero()).isTrue(); } @@ -60,19 +62,22 @@ class DefaultNeo4jConversionServiceTest { void shouldDealWith0IsoDurationsAsDurations() { Value zeroDuration = Values.isoDuration(0, 0, 0, 0); - Duration duration = (Duration) defaultNeo4jEntityAccessor.readValue(zeroDuration, TypeInformation.of(Duration.class), null); + Duration duration = (Duration) DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor + .readValue(zeroDuration, TypeInformation.of(Duration.class), null); assertThat(duration).isZero(); } @Test // GH-2324 void shouldDealWithNullTemporalValueOnRead() { - Duration duration = (Duration) defaultNeo4jEntityAccessor.readValue(null, TypeInformation.of(Duration.class), null); + Duration duration = (Duration) DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor + .readValue(null, TypeInformation.of(Duration.class), null); assertThat(duration).isNull(); } @Test // GH-2324 void shouldDealWithNullTemporalValueOnWrite() { - Value value = defaultNeo4jEntityAccessor.writeValue(null, TypeInformation.of(TemporalAmount.class), null); + Value value = DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor.writeValue(null, + TypeInformation.of(TemporalAmount.class), null); assertThat(value).isNull(); } @@ -81,9 +86,11 @@ class DefaultNeo4jConversionServiceTest { Value value = Values.value("Das funktioniert nicht."); assertThatExceptionOfType(TypeMismatchDataAccessException.class) - .isThrownBy(() -> defaultNeo4jEntityAccessor.readValue(value, TypeInformation.of(Date.class), null)) - .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.util.Date") - .withCauseInstanceOf(ConversionFailedException.class).withRootCauseInstanceOf(DateTimeParseException.class); + .isThrownBy(() -> DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor.readValue(value, + TypeInformation.of(Date.class), null)) + .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.util.Date") + .withCauseInstanceOf(ConversionFailedException.class) + .withRootCauseInstanceOf(DateTimeParseException.class); } @Test @@ -91,21 +98,25 @@ class DefaultNeo4jConversionServiceTest { Value value = Values.value("Das funktioniert nicht."); assertThatExceptionOfType(TypeMismatchDataAccessException.class) - .isThrownBy( - () -> defaultNeo4jEntityAccessor.readValue(value, TypeInformation.of(LocalDate.class), null)) - .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.time.LocalDate") - .withCauseInstanceOf(ConversionFailedException.class).withRootCauseInstanceOf(Uncoercible.class); + .isThrownBy(() -> DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor.readValue(value, + TypeInformation.of(LocalDate.class), null)) + .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.time.LocalDate") + .withCauseInstanceOf(ConversionFailedException.class) + .withRootCauseInstanceOf(Uncoercible.class); } @Test void shouldCatchCoercibleErrors() { Value value = Values.value("Das funktioniert nicht."); - assertThatExceptionOfType(TypeMismatchDataAccessException.class).isThrownBy( - () -> defaultNeo4jEntityAccessor.readValue(value, TypeInformation.of(ReactiveNeo4jClient.class), null)) - .withMessageStartingWith( - "Could not convert \"Das funktioniert nicht.\" into org.springframework.data.neo4j.core.ReactiveNeo4jClient") - .withRootCauseInstanceOf(ConverterNotFoundException.class); + assertThatExceptionOfType(TypeMismatchDataAccessException.class) + .isThrownBy(() -> DefaultNeo4jConversionServiceTests.this.defaultNeo4jEntityAccessor.readValue(value, + TypeInformation.of(ReactiveNeo4jClient.class), null)) + .withMessageStartingWith( + "Could not convert \"Das funktioniert nicht.\" into org.springframework.data.neo4j.core.ReactiveNeo4jClient") + .withRootCauseInstanceOf(ConverterNotFoundException.class); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTests.java similarity index 84% rename from src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTests.java index 5b18328b7..97012e0b9 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverterTests.java @@ -15,6 +15,10 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; import org.neo4j.driver.Values; @@ -22,6 +26,7 @@ import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.value.NodeValue; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.neo4j.core.convert.Neo4jConversions; @@ -31,39 +36,36 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.util.TypeInformation; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; /** * @author Gerrit Meier */ -class DefaultNeo4jEntityConverterTest { +class DefaultNeo4jEntityConverterTests { private final DefaultNeo4jEntityConverter entityConverter; - DefaultNeo4jEntityConverterTest() { + DefaultNeo4jEntityConverterTests() { EntityInstantiators entityInstantiators = new EntityInstantiators(); NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore(); DefaultNeo4jConversionService conversionService = new DefaultNeo4jConversionService(new Neo4jConversions()); Neo4jMappingContext context = new Neo4jMappingContext(); context.addPersistentEntity(TypeInformation.of(EntityWithDefaultValues.class)); - nodeDescriptionStore.put("User", (DefaultNeo4jPersistentEntity) context.getNodeDescription(EntityWithDefaultValues.class)); + nodeDescriptionStore.put("User", + (DefaultNeo4jPersistentEntity) context.getNodeDescription(EntityWithDefaultValues.class)); EventSupport eventSupport = EventSupport.useExistingCallbacks(context, EntityCallbacks.create()); TypeSystem typeSystem = InternalTypeSystem.TYPE_SYSTEM; - this.entityConverter = new DefaultNeo4jEntityConverter(entityInstantiators, nodeDescriptionStore, conversionService, eventSupport, typeSystem); + this.entityConverter = new DefaultNeo4jEntityConverter(entityInstantiators, nodeDescriptionStore, + conversionService, eventSupport, typeSystem); } @Test void readEntityWithDefaultValuesWithEmptyPropertiesFromDatabase() { Map properties = new HashMap<>(); NodeValue mapAccessor = new NodeValue( - new InternalNode(1L, Collections.singleton("EntityWithDefaultValues"), properties) - ); + new InternalNode(1L, Collections.singleton("EntityWithDefaultValues"), properties)); - EntityWithDefaultValues readNode = entityConverter.read(EntityWithDefaultValues.class, mapAccessor); + EntityWithDefaultValues readNode = this.entityConverter.read(EntityWithDefaultValues.class, mapAccessor); assertThat(readNode).isNotNull(); assertThat(readNode.noDefaultValue).isNull(); assertThat(readNode.defaultValue).isEqualTo("Test"); @@ -75,10 +77,9 @@ class DefaultNeo4jEntityConverterTest { properties.put("noDefaultValue", Values.value("valueFromDatabase1")); properties.put("defaultValue", Values.value("valueFromDatabase2")); NodeValue mapAccessor = new NodeValue( - new InternalNode(1L, Collections.singleton("EntityWithDefaultValues"), properties) - ); + new InternalNode(1L, Collections.singleton("EntityWithDefaultValues"), properties)); - EntityWithDefaultValues readNode = entityConverter.read(EntityWithDefaultValues.class, mapAccessor); + EntityWithDefaultValues readNode = this.entityConverter.read(EntityWithDefaultValues.class, mapAccessor); assertThat(readNode).isNotNull(); assertThat(readNode.noDefaultValue).isEqualTo("valueFromDatabase1"); assertThat(readNode.defaultValue).isEqualTo("valueFromDatabase2"); @@ -86,8 +87,15 @@ class DefaultNeo4jEntityConverterTest { @Node static class EntityWithDefaultValues { - @Id @GeneratedValue Long id; + public String noDefaultValue; + public String defaultValue = "Test"; + + @Id + @GeneratedValue + Long id; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java deleted file mode 100644 index e83557a91..000000000 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2011-2025 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.core.mapping; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.mapping.IdentifierAccessor; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.neo4j.core.schema.IdGenerator; -import org.springframework.data.support.IsNewStrategy; - -/** - * @author Michael J. Simons - */ -@ExtendWith(MockitoExtension.class) -class DefaultNeo4jIsNewStrategyTest { - - @Mock Neo4jPersistentEntity entityMetaData; - - @Mock Neo4jPersistentProperty idProperty; - - @Mock Neo4jPersistentProperty versionProperty; - - @Nested - class InternallyGenerated { - @Test - void shouldDealWithNonPrimitives() { - Object a = new Object(); - Object b = new Object(); - - IdDescription idDescription = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE); - doReturn(Long.class).when(idProperty).getType(); - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - doReturn((IdentifierAccessor) () -> null).when(entityMetaData).getIdentifierAccessor(a); - doReturn((IdentifierAccessor) () -> Long.valueOf(1)).when(entityMetaData).getIdentifierAccessor(b); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - assertThat(strategy.isNew(a)).isTrue(); - assertThat(strategy.isNew(b)).isFalse(); - } - - @Test - void shouldDealWithPrimitives() { - Object a = new Object(); - Object b = new Object(); - Object c = new Object(); - - IdDescription idDescription = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE); - doReturn(long.class).when(idProperty).getType(); - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - doReturn((IdentifierAccessor) () -> -1L).when(entityMetaData).getIdentifierAccessor(a); - doReturn((IdentifierAccessor) () -> 0L).when(entityMetaData).getIdentifierAccessor(b); - doReturn((IdentifierAccessor) () -> 1L).when(entityMetaData).getIdentifierAccessor(c); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - assertThat(strategy.isNew(a)).isTrue(); - assertThat(strategy.isNew(b)).isFalse(); - assertThat(strategy.isNew(c)).isFalse(); - } - } - - @Nested - class ExternallyGenerated { - @Test - void shouldDealWithNonPrimitives() { - - Object a = new Object(); - Object b = new Object(); - IdDescription idDescription = IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "na"); - doReturn(String.class).when(idProperty).getType(); - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - doReturn((IdentifierAccessor) () -> null).when(entityMetaData).getIdentifierAccessor(a); - doReturn((IdentifierAccessor) () -> "4711").when(entityMetaData).getIdentifierAccessor(b); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - assertThat(strategy.isNew(a)).isTrue(); - assertThat(strategy.isNew(b)).isFalse(); - } - - @Test - void doesntNeedToDealWithPrimitives() { - - IdDescription idDescription = IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "na"); - doReturn(long.class).when(idProperty).getType(); - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - - assertThatIllegalArgumentException().isThrownBy(() -> DefaultNeo4jIsNewStrategy.basedOn(entityMetaData)) - .withMessage( - "Cannot use org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy with externally generated, primitive ids"); - } - } - - @Nested - class Assigned { - - @Test - void shouldAlwaysTreatEntitiesAsNewWithoutVersion() { - Object a = new Object(); - IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); - doReturn(String.class).when(idProperty).getType(); - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - assertThat(strategy.isNew(a)).isTrue(); - } - - @Test - void shouldDealWithVersion() { - Object a = new Object(); - Object b = new Object(); - IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); - - doReturn(String.class).when(idProperty).getType(); - doReturn(String.class).when(versionProperty).getType(); - - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - doReturn(versionProperty).when(entityMetaData).getVersionProperty(); - - PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); - doReturn(null).when(aa).getProperty(versionProperty); - doReturn(aa).when(entityMetaData).getPropertyAccessor(a); - - PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); - doReturn("A version").when(ab).getProperty(versionProperty); - doReturn(ab).when(entityMetaData).getPropertyAccessor(b); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - - assertThat(strategy.isNew(a)).isTrue(); - assertThat(strategy.isNew(b)).isFalse(); - } - - @Test - void shouldDealWithPrimitiveVersion() { - Object a = new Object(); - Object b = new Object(); - IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); - - doReturn(String.class).when(idProperty).getType(); - doReturn(int.class).when(versionProperty).getType(); - - doReturn(idDescription).when(entityMetaData).getIdDescription(); - doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); - doReturn(versionProperty).when(entityMetaData).getVersionProperty(); - - PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); - doReturn(0).when(aa).getProperty(versionProperty); - doReturn(aa).when(entityMetaData).getPropertyAccessor(a); - - PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); - doReturn(1).when(ab).getProperty(versionProperty); - doReturn(ab).when(entityMetaData).getPropertyAccessor(b); - - IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); - - assertThat(strategy.isNew(a)).isTrue(); - assertThat(strategy.isNew(b)).isFalse(); - } - } - - static class DummyIdGenerator implements IdGenerator { - - @Override - public Void generateId(String primaryLabel, Object entity) { - return null; - } - } -} diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTests.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTests.java new file mode 100644 index 000000000..b55c2cb54 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2011-2025 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.core.mapping; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.neo4j.core.schema.IdGenerator; +import org.springframework.data.support.IsNewStrategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class DefaultNeo4jIsNewStrategyTests { + + @Mock + Neo4jPersistentEntity entityMetaData; + + @Mock + Neo4jPersistentProperty idProperty; + + @Mock + Neo4jPersistentProperty versionProperty; + + static class DummyIdGenerator implements IdGenerator { + + @Override + public Void generateId(String primaryLabel, Object entity) { + return null; + } + + } + + @Nested + class InternallyGenerated { + + @Test + void shouldDealWithNonPrimitives() { + Object a = new Object(); + Object b = new Object(); + + IdDescription idDescription = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE); + doReturn(Long.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> null).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> Long.valueOf(1)) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void shouldDealWithPrimitives() { + Object a = new Object(); + Object b = new Object(); + Object c = new Object(); + + IdDescription idDescription = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE); + doReturn(long.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> -1L).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> 0L).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(b); + doReturn((IdentifierAccessor) () -> 1L).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(c); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + assertThat(strategy.isNew(c)).isFalse(); + } + + } + + @Nested + class ExternallyGenerated { + + @Test + void shouldDealWithNonPrimitives() { + + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, + DummyIdGenerator.class, null, "na"); + doReturn(String.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> null).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> "4711").when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getIdentifierAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void doesntNeedToDealWithPrimitives() { + + IdDescription idDescription = IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, + DummyIdGenerator.class, null, "na"); + doReturn(long.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> DefaultNeo4jIsNewStrategy.basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData)) + .withMessage( + "Cannot use org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy with externally generated, primitive ids"); + } + + } + + @Nested + class Assigned { + + @Test + void shouldAlwaysTreatEntitiesAsNewWithoutVersion() { + Object a = new Object(); + IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); + doReturn(String.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + } + + @Test + void shouldDealWithVersion() { + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); + + doReturn(String.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(String.class).when(DefaultNeo4jIsNewStrategyTests.this.versionProperty).getType(); + + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.versionProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getVersionProperty(); + + PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); + doReturn(null).when(aa).getProperty(DefaultNeo4jIsNewStrategyTests.this.versionProperty); + doReturn(aa).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getPropertyAccessor(a); + + PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); + doReturn("A version").when(ab).getProperty(DefaultNeo4jIsNewStrategyTests.this.versionProperty); + doReturn(ab).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getPropertyAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void shouldDealWithPrimitiveVersion() { + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "na"); + + doReturn(String.class).when(DefaultNeo4jIsNewStrategyTests.this.idProperty).getType(); + doReturn(int.class).when(DefaultNeo4jIsNewStrategyTests.this.versionProperty).getType(); + + doReturn(idDescription).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getIdDescription(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.idProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getRequiredIdProperty(); + doReturn(DefaultNeo4jIsNewStrategyTests.this.versionProperty) + .when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData) + .getVersionProperty(); + + PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); + doReturn(0).when(aa).getProperty(DefaultNeo4jIsNewStrategyTests.this.versionProperty); + doReturn(aa).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getPropertyAccessor(a); + + PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); + doReturn(1).when(ab).getProperty(DefaultNeo4jIsNewStrategyTests.this.versionProperty); + doReturn(ab).when(DefaultNeo4jIsNewStrategyTests.this.entityMetaData).getPropertyAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy + .basedOn(DefaultNeo4jIsNewStrategyTests.this.entityMetaData); + + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTests.java similarity index 68% rename from src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTests.java index f212a5418..06cce7431 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTests.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -30,6 +26,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Vector; @@ -46,11 +43,15 @@ import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * @author Gerrit Meier * @author Michael J. Simons */ -class DefaultNeo4jPersistentEntityTest { +class DefaultNeo4jPersistentEntityTests { @Test void persistentEntityCreationWorksForCorrectEntity() { @@ -62,13 +63,513 @@ class DefaultNeo4jPersistentEntityTest { @Test void skipsEntityTypeDetectionForConvertedProperties() { - Neo4jPersistentEntity entity = new Neo4jMappingContext().getRequiredPersistentEntity(WithConvertedProperty.class); + Neo4jPersistentEntity entity = new Neo4jMappingContext() + .getRequiredPersistentEntity(WithConvertedProperty.class); Neo4jPersistentProperty property = entity.getRequiredPersistentProperty("converted"); assertThat(property.isEntity()).isFalse(); assertThat(property.getPersistentEntityTypeInformation()).isEmpty(); } + @Node + private static final class SomeOtherNode { + + @Id + Long id; + + } + + @Node + private static class NodeWithDynamicLabels { + + @Id + @GeneratedValue + Long id; + + List relatedTo; + + @DynamicLabels + List dynamicLabels; + + } + + @Node + private static final class NodeWithInvalidDynamicLabels { + + @Id + @GeneratedValue + Long id; + + @DynamicLabels + List dynamicLabels; + + @DynamicLabels + List moarDynamicLabels; + + } + + @Node + private static final class ValidInheritedDynamicLabels extends NodeWithDynamicLabels { + + } + + @Node + private static final class InvalidInheritedDynamicLabels extends NodeWithDynamicLabels { + + @DynamicLabels + List localDynamicLabels; + + } + + @Node + private static final class InvalidDynamicLabels { + + @Id + @GeneratedValue + Long id; + + @DynamicLabels + String dynamicLabels; + + } + + @Node + private static final class CorrectEntity1 { + + @Id + private Long id; + + private String name; + + private Map dynamicRelationships; + + } + + @Node + private static final class CorrectEntity2 { + + @Id + private Long id; + + private String name; + + @Relationship(direction = Relationship.Direction.INCOMING) + private Map dynamicRelationships; + + } + + @Node + private static final class MixedDynamicAndExplicitRelationship1 { + + @Id + private Long id; + + private String name; + + @Relationship(type = "BAMM") + private Map dynamicRelationships; + + } + + @Node + private static final class MixedDynamicAndExplicitRelationship2 { + + @Id + private Long id; + + private String name; + + @Relationship(type = "BAMM", direction = Relationship.Direction.INCOMING) + private Map> dynamicRelationships; + + } + + @Node + private static final class EntityWithDuplicatedProperties { + + @Id + private Long id; + + private String name; + + @Property("name") + private String alsoName; + + } + + @Node + private static final class EntityWithMultipleDuplicatedProperties { + + @Id + private Long id; + + private String name; + + @Property("name") + private String alsoName; + + @Property("foo") + private String somethingElse; + + @Property("foo") + private String thisToo; + + } + + private abstract static class BaseClassWithPrivatePropertyUnsafe { + + @Id + @GeneratedValue + private Long id; + + private String name; + + } + + @Node + private static final class EntityWithInheritedMultipleDuplicatedProperties + extends BaseClassWithPrivatePropertyUnsafe { + + private String name; + + } + + private abstract static class BaseClassWithPrivatePropertySafe { + + @Id + @GeneratedValue + private Long id; + + @Transient + private String name; + + } + + @Node + private static final class EntityWithNotInheritedTransientProperties extends BaseClassWithPrivatePropertySafe { + + private String name; + + } + + @Node("a") + private static final class EntityWithSingleLabel { + + @Id + private Long id; + + } + + @Node({ "a", "b", "c" }) + private static final class EntityWithMultipleLabels { + + @Id + private Long id; + + } + + @Node(primaryLabel = "a") + private static final class EntityWithExplicitPrimaryLabel { + + @Id + private Long id; + + } + + @Node(primaryLabel = "a", labels = { "b", "c" }) + private static final class EntityWithExplicitPrimaryLabelAndAdditionalLabels { + + @Id + private Long id; + + } + + @Node(primaryLabel = "Base", labels = { "Bases" }) + private abstract static class BaseClass { + + @Id + private Long id; + + } + + @Node(primaryLabel = "Child", labels = { "Person" }) + private static final class Child extends BaseClass { + + private String name; + + } + + @Node + static class TypeWithInvalidDynamicRelationshipMappings1 { + + @Id + private String id; + + private Map bikes1; + + private Map bikes2; + + } + + @Node + static class TypeWithInvalidDynamicRelationshipMappings2 { + + @Id + private String id; + + private Map bikes1; + + private Map> bikes2; + + } + + @Node + static class TypeWithInvalidDynamicRelationshipMappings3 { + + @Id + private String id; + + private Map> bikes1; + + private Map> bikes2; + + } + + @Node + static class EntityWithCorrectRelationshipProperties { + + @Relationship + HasTargetNodeRelationshipProperties rel; + + @Id + private String id; + + } + + @Node + static class EntityWithInCorrectRelationshipProperties { + + @Relationship + HasNoTargetNodeRelationshipProperties rel; + + @Id + private String id; + + } + + @RelationshipProperties + static class HasTargetNodeRelationshipProperties { + + @TargetNode + EntityWithExplicitPrimaryLabel entity; + + @RelationshipId + private Long id; + + } + + @RelationshipProperties + static class HasNoTargetNodeRelationshipProperties { + + @RelationshipId + private Long id; + + } + + @Node + static class EntityWithBidirectionalRelationship { + + @Relationship("KNOWS") + List knows; + + @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) + List knownBy; + + @Id + @GeneratedValue + private Long id; + + } + + @Node + static class EntityWithBidirectionalRelationshipToOtherEntity { + + @Relationship("KNOWS") + List knows; + + @Id + @GeneratedValue + private Long id; + + } + + @Node + static class OtherEntityWithBidirectionalRelationship { + + @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) + List knownBy; + + @Id + @GeneratedValue + private Long id; + + } + + @Node + static class EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties { + + @Relationship("KNOWS") + List knows; + + @Id + @GeneratedValue + private Long id; + + } + + @Node + static class OtherEntityWithBidirectionalRelationshipWithRelationshipProperties { + + @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) + List knownBy; + + @Id + @GeneratedValue + private Long id; + + } + + @RelationshipProperties + static class OtherEntityWithBidirectionalRelationshipWithRelationshipPropertiesProperties { + + @TargetNode + OtherEntityWithBidirectionalRelationshipWithRelationshipProperties target; + + @RelationshipId + private Long id; + + } + + @RelationshipProperties + static class EntityWithBidirectionalRelationshipWithRelationshipPropertiesProperties { + + @TargetNode + EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties target; + + @RelationshipId + private Long id; + + } + + @Node + static class EntityWithBidirectionalRelationshipProperties { + + @Relationship("KNOWS") + List knows; + + @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) + List knownBy; + + @Id + @GeneratedValue + private Long id; + + } + + @RelationshipProperties + static class BidirectionalRelationshipProperties { + + @TargetNode + EntityWithBidirectionalRelationshipProperties target; + + @RelationshipId + private Long id; + + } + + @Node + static class EntityLooksLikeHasObserve { + + @Id + @GeneratedValue + private Long id; + + @Relationship("KNOWS") + private List knows; + + } + + @Node + static class OtherEntityLooksLikeHasObserve { + + @Id + @GeneratedValue + private Long id; + + @Relationship("KNOWS") + private List knows; + + } + + @Node + static class WithAnnotatedProperties { + + @Id + @GeneratedValue + private Long id; + + private String defaultProperty; + + @Property + private String defaultAnnotatedProperty; + + @Property(readOnly = true) + private String readOnlyProperty; + + @ReadOnlyProperty + private String usingSpringsAnnotation; + + @SuppressWarnings("DefaultAnnotationParam") + @Property(readOnly = false) + private String writableProperty; + + } + + static class WithConvertedProperty { + + @ConvertWith + IWillBeConverted converted; + + } + + static class IWillBeConverted { + + } + + @Node + static class VectorValid { + + Vector vectorProperty; + + @Id + @GeneratedValue + private Long id; + + } + + @Node + static class VectorInvalid { + + Vector vectorProperty1; + + Vector vectorProperty2; + + @Id + @GeneratedValue + private Long id; + + } + @Nested class ReadOnlyProperties { @@ -76,76 +577,83 @@ class DefaultNeo4jPersistentEntityTest { ReadOnlyProperties() { - neo4jMappingContext = new Neo4jMappingContext(); - neo4jMappingContext.setInitialEntitySet(Collections.singleton(WithAnnotatedProperties.class)); + this.neo4jMappingContext = new Neo4jMappingContext(); + this.neo4jMappingContext.setInitialEntitySet(Collections.singleton(WithAnnotatedProperties.class)); } @ParameterizedTest // GH-2376 - @ValueSource(strings = {"defaultProperty", "defaultAnnotatedProperty", "writableProperty"}) + @ValueSource(strings = { "defaultProperty", "defaultAnnotatedProperty", "writableProperty" }) void propertiesShouldBeWritable(String propertyName) { - Neo4jPersistentProperty property = neo4jMappingContext.getPersistentEntity(WithAnnotatedProperties.class) - .getRequiredPersistentProperty(propertyName); + Neo4jPersistentProperty property = this.neo4jMappingContext + .getPersistentEntity(WithAnnotatedProperties.class) + .getRequiredPersistentProperty(propertyName); assertThat(property.isReadOnly()).isFalse(); } @ParameterizedTest // GH-2376, GH-2294 - @ValueSource(strings = {"readOnlyProperty", "usingSpringsAnnotation"}) + @ValueSource(strings = { "readOnlyProperty", "usingSpringsAnnotation" }) void propertiesShouldBeReadOnly(String propertyName) { - Neo4jPersistentProperty property = neo4jMappingContext.getPersistentEntity(WithAnnotatedProperties.class) - .getRequiredPersistentProperty(propertyName); + Neo4jPersistentProperty property = this.neo4jMappingContext + .getPersistentEntity(WithAnnotatedProperties.class) + .getRequiredPersistentProperty(propertyName); assertThat(property.isReadOnly()).isTrue(); } + } @Nested class DuplicateProperties { + @Test void failsOnDuplicatedProperties() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(EntityWithDuplicatedProperties.class)) - .withMessage("Duplicate definition of property [name] in entity class " - + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTest$EntityWithDuplicatedProperties"); + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(EntityWithDuplicatedProperties.class)) + .withMessage("Duplicate definition of property [name] in entity class " + + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTests$EntityWithDuplicatedProperties"); } @Test void failsOnMultipleDuplicatedProperties() { - assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(EntityWithMultipleDuplicatedProperties.class)) - .withMessage("Duplicate definition of properties [foo, name] in entity class " - + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTest$EntityWithMultipleDuplicatedProperties"); + assertThatIllegalStateException().isThrownBy( + () -> new Neo4jMappingContext().getPersistentEntity(EntityWithMultipleDuplicatedProperties.class)) + .withMessage("Duplicate definition of properties [foo, name] in entity class " + + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTests$EntityWithMultipleDuplicatedProperties"); } @Test // GH-1903 void failsOnMultipleInheritedDuplicatedProperties() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity( - EntityWithInheritedMultipleDuplicatedProperties.class)) - .withMessage("Duplicate definition of property [name] in entity class " - + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTest$EntityWithInheritedMultipleDuplicatedProperties"); + .isThrownBy(() -> new Neo4jMappingContext() + .getPersistentEntity(EntityWithInheritedMultipleDuplicatedProperties.class)) + .withMessage("Duplicate definition of property [name] in entity class " + + "org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTests$EntityWithInheritedMultipleDuplicatedProperties"); } @Test // GH-1903 void doesNotFailOnTransientInheritedDuplicatedProperties() { - Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext().getPersistentEntity( - EntityWithNotInheritedTransientProperties.class); + Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() + .getPersistentEntity(EntityWithNotInheritedTransientProperties.class); assertThat(persistentEntity).isNotNull(); assertThat(persistentEntity.getPersistentProperty("name")).isNotNull(); } + } @Nested class Relationships { @ParameterizedTest - @ValueSource(classes = { MixedDynamicAndExplicitRelationship1.class, MixedDynamicAndExplicitRelationship2.class }) + @ValueSource( + classes = { MixedDynamicAndExplicitRelationship1.class, MixedDynamicAndExplicitRelationship2.class }) void failsOnDynamicRelationshipsWithExplicitType(Class entityToTest) { String expectedMessage = "Dynamic relationships cannot be used with a fixed type\\; omit @Relationship or use @Relationship\\(direction = (OUTGOING|INCOMING)\\) without a type in class .*MixedDynamicAndExplicitRelationship\\d on field dynamicRelationships"; - assertThatIllegalStateException().isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(entityToTest)) - .withMessageMatching(expectedMessage); + assertThatIllegalStateException() + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(entityToTest)) + .withMessageMatching(expectedMessage); } @ParameterizedTest // GH-216 @@ -153,16 +661,17 @@ class DefaultNeo4jPersistentEntityTest { TypeWithInvalidDynamicRelationshipMappings2.class, TypeWithInvalidDynamicRelationshipMappings3.class }) void multipleDynamicAssociationsToTheSameEntityAreNotAllowed(Class entityToTest) { - String expectedMessage = ".*TypeWithInvalidDynamicRelationshipMappings\\d already contains a dynamic relationship to class org\\.springframework\\.data\\.neo4j\\.core\\.mapping\\.Neo4jMappingContextTest\\$BikeNode; only one dynamic relationship between to entities is permitted"; + String expectedMessage = ".*TypeWithInvalidDynamicRelationshipMappings\\d already contains a dynamic relationship to class org\\.springframework\\.data\\.neo4j\\.core\\.mapping\\.Neo4jMappingContextTests\\$BikeNode; only one dynamic relationship between to entities is permitted"; Neo4jMappingContext schema = new Neo4jMappingContext(); schema.setInitialEntitySet(new HashSet<>(Arrays.asList(entityToTest))); - assertThatIllegalStateException().isThrownBy(() -> schema.initialize()).withMessageMatching(expectedMessage); + assertThatIllegalStateException().isThrownBy(() -> schema.initialize()) + .withMessageMatching(expectedMessage); } @Test // DATAGRAPH-1420 void doesNotFailOnCorrectRelationshipProperties() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(EntityWithCorrectRelationshipProperties.class); + .getPersistentEntity(EntityWithCorrectRelationshipProperties.class); assertThat(persistentEntity).isNotNull(); } @@ -171,15 +680,16 @@ class DefaultNeo4jPersistentEntityTest { void doesFailOnRelationshipPropertiesWithMissingTargetNode() { assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> new Neo4jMappingContext() - .getPersistentEntity(EntityWithInCorrectRelationshipProperties.class)) - .withMessageContaining("Missing @TargetNode declaration in"); + .isThrownBy(() -> new Neo4jMappingContext() + .getPersistentEntity(EntityWithInCorrectRelationshipProperties.class)) + .withMessageContaining("Missing @TargetNode declaration in"); } @Test // GH-2289 void correctlyFindRelationshipObverseSameEntity() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity persistentEntity = neo4jMappingContext.getPersistentEntity(EntityWithBidirectionalRelationship.class); + Neo4jPersistentEntity persistentEntity = neo4jMappingContext + .getPersistentEntity(EntityWithBidirectionalRelationship.class); persistentEntity.doWithAssociations((AssociationHandler) a -> { RelationshipDescription rd = (RelationshipDescription) a; assertThat(rd.getRelationshipObverse()).isNotNull(); @@ -189,7 +699,8 @@ class DefaultNeo4jPersistentEntityTest { @Test // GH-2289 void correctlyFindRelationshipObverse() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity persistentEntity = neo4jMappingContext.getPersistentEntity(EntityWithBidirectionalRelationshipToOtherEntity.class); + Neo4jPersistentEntity persistentEntity = neo4jMappingContext + .getPersistentEntity(EntityWithBidirectionalRelationshipToOtherEntity.class); persistentEntity.doWithAssociations((AssociationHandler) a -> { RelationshipDescription rd = (RelationshipDescription) a; assertThat(rd.getRelationshipObverse()).isNotNull(); @@ -204,7 +715,8 @@ class DefaultNeo4jPersistentEntityTest { @Test // GH-2289 void correctlyFindRelationshipObverseWithRelationshipProperties() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity persistentEntity = neo4jMappingContext.getPersistentEntity(EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties.class); + Neo4jPersistentEntity persistentEntity = neo4jMappingContext + .getPersistentEntity(EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties.class); persistentEntity.doWithAssociations((AssociationHandler) a -> { RelationshipDescription rd = (RelationshipDescription) a; assertThat(rd.getRelationshipObverse()).isNotNull(); @@ -219,7 +731,8 @@ class DefaultNeo4jPersistentEntityTest { @Test // GH-2289 void correctlyFindSameEntityRelationshipObverseWithRelationshipProperties() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity persistentEntity = neo4jMappingContext.getPersistentEntity(EntityWithBidirectionalRelationshipProperties.class); + Neo4jPersistentEntity persistentEntity = neo4jMappingContext + .getPersistentEntity(EntityWithBidirectionalRelationshipProperties.class); persistentEntity.doWithAssociations((AssociationHandler) a -> { RelationshipDescription rd = (RelationshipDescription) a; assertThat(rd.getRelationshipObverse()).isNotNull(); @@ -229,7 +742,8 @@ class DefaultNeo4jPersistentEntityTest { @Test // GH-2289 void correctlyDontFindRelationshipObverse() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity persistentEntity = neo4jMappingContext.getPersistentEntity(EntityLooksLikeHasObserve.class); + Neo4jPersistentEntity persistentEntity = neo4jMappingContext + .getPersistentEntity(EntityLooksLikeHasObserve.class); persistentEntity.doWithAssociations((AssociationHandler) a -> { RelationshipDescription rd = (RelationshipDescription) a; assertThat(rd.getRelationshipObverse()).isNull(); @@ -240,6 +754,7 @@ class DefaultNeo4jPersistentEntityTest { assertThat(rd.getRelationshipObverse()).isNull(); }); } + } @Nested @@ -248,7 +763,8 @@ class DefaultNeo4jPersistentEntityTest { @Test void supportDerivedLabel() { - Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext().getPersistentEntity(CorrectEntity1.class); + Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() + .getPersistentEntity(CorrectEntity1.class); assertThat(persistentEntity.getPrimaryLabel()).isEqualTo("CorrectEntity1"); assertThat(persistentEntity.getAdditionalLabels()).isEmpty(); @@ -258,7 +774,7 @@ class DefaultNeo4jPersistentEntityTest { void supportSingleLabel() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(EntityWithSingleLabel.class); + .getPersistentEntity(EntityWithSingleLabel.class); assertThat(persistentEntity.getPrimaryLabel()).isEqualTo("a"); assertThat(persistentEntity.getAdditionalLabels()).isEmpty(); @@ -268,7 +784,7 @@ class DefaultNeo4jPersistentEntityTest { void supportMultipleLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(EntityWithMultipleLabels.class); + .getPersistentEntity(EntityWithMultipleLabels.class); assertThat(persistentEntity.getPrimaryLabel()).isEqualTo("a"); assertThat(persistentEntity.getAdditionalLabels()).containsExactlyInAnyOrder("b", "c"); @@ -278,7 +794,7 @@ class DefaultNeo4jPersistentEntityTest { void supportExplicitPrimaryLabel() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(EntityWithExplicitPrimaryLabel.class); + .getPersistentEntity(EntityWithExplicitPrimaryLabel.class); assertThat(persistentEntity.getPrimaryLabel()).isEqualTo("a"); assertThat(persistentEntity.getAdditionalLabels()).isEmpty(); @@ -288,7 +804,7 @@ class DefaultNeo4jPersistentEntityTest { void supportExplicitPrimaryLabelAndAdditionalLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(EntityWithExplicitPrimaryLabelAndAdditionalLabels.class); + .getPersistentEntity(EntityWithExplicitPrimaryLabelAndAdditionalLabels.class); assertThat(persistentEntity.getPrimaryLabel()).isEqualTo("a"); assertThat(persistentEntity.getAdditionalLabels()).containsExactlyInAnyOrder("b", "c"); @@ -309,7 +825,7 @@ class DefaultNeo4jPersistentEntityTest { void validDynamicLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(NodeWithDynamicLabels.class); + .getPersistentEntity(NodeWithDynamicLabels.class); assertThat(persistentEntity.getGraphProperties()).hasSize(2); assertThat(persistentEntity.getPersistentProperty("id").isIdProperty()).isTrue(); @@ -325,14 +841,14 @@ class DefaultNeo4jPersistentEntityTest { Assertions.assertThat(persistentEntity.getPersistentProperty("dynamicLabels").isRelationship()).isFalse(); assertThat(persistentEntity.getDynamicLabelsProperty()) - .hasValueSatisfying(p -> p.getFieldName().equals("dynamicLabels")); + .hasValueSatisfying(p -> p.getFieldName().equals("dynamicLabels")); } @Test void shouldDetectValidInheritedDynamicLabels() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(ValidInheritedDynamicLabels.class); + .getPersistentEntity(ValidInheritedDynamicLabels.class); assertThat(persistentEntity.getGraphProperties()).hasSize(2); assertThat(persistentEntity.getPersistentProperty("id").isIdProperty()).isTrue(); @@ -348,43 +864,45 @@ class DefaultNeo4jPersistentEntityTest { Assertions.assertThat(persistentEntity.getPersistentProperty("dynamicLabels").isRelationship()).isFalse(); assertThat(persistentEntity.getDynamicLabelsProperty()) - .hasValueSatisfying(p -> p.getFieldName().equals("dynamicLabels")); + .hasValueSatisfying(p -> p.getFieldName().equals("dynamicLabels")); } @Test void shouldDetectInvalidInheritedDynamicLabels() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(InvalidInheritedDynamicLabels.class)) - .withMessageMatching( - "Multiple properties in entity class .*DefaultNeo4jPersistentEntityTest\\$InvalidInheritedDynamicLabels are annotated with @DynamicLabels: \\[dynamicLabels, localDynamicLabels]"); + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(InvalidInheritedDynamicLabels.class)) + .withMessageMatching( + "Multiple properties in entity class .*DefaultNeo4jPersistentEntityTests\\$InvalidInheritedDynamicLabels are annotated with @DynamicLabels: \\[dynamicLabels, localDynamicLabels]"); } @Test void shouldDetectInvalidDynamicLabels() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(NodeWithInvalidDynamicLabels.class)) - .withMessageMatching( - "Multiple properties in entity class .*DefaultNeo4jPersistentEntityTest\\$NodeWithInvalidDynamicLabels are annotated with @DynamicLabels: \\[dynamicLabels, moarDynamicLabels]"); + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(NodeWithInvalidDynamicLabels.class)) + .withMessageMatching( + "Multiple properties in entity class .*DefaultNeo4jPersistentEntityTests\\$NodeWithInvalidDynamicLabels are annotated with @DynamicLabels: \\[dynamicLabels, moarDynamicLabels]"); } @Test void shouldDetectInvalidDynamicLabelsTarget() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(InvalidDynamicLabels.class)) - .withMessageMatching( - "Property dynamicLabels on class .*DefaultNeo4jPersistentEntityTest\\$InvalidDynamicLabels must extends java\\.util\\.Collection"); + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(InvalidDynamicLabels.class)) + .withMessageMatching( + "Property dynamicLabels on class .*DefaultNeo4jPersistentEntityTests\\$InvalidDynamicLabels must extends java\\.util\\.Collection"); } + } @Nested class VectorType { + @Test void validVectorProperties() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() - .getPersistentEntity(VectorValid.class); + .getPersistentEntity(VectorValid.class); assertThat(persistentEntity.getPersistentProperty("vectorProperty").isVectorProperty()); } @@ -392,402 +910,14 @@ class DefaultNeo4jPersistentEntityTest { @Test void invalidVectorProperties() { assertThatIllegalStateException() - .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(VectorInvalid.class)) - .withMessageContaining("There are multiple fields of type interface org.springframework.data.domain.Vector in entity org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTest$VectorInvalid:") - // the order of properties might be not the same all the time - .withMessageContaining("vectorProperty1") - .withMessageContaining("vectorProperty2"); + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(VectorInvalid.class)) + .withMessageContaining( + "There are multiple fields of type interface org.springframework.data.domain.Vector in entity org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTests$VectorInvalid:") + // the order of properties might be not the same all the time + .withMessageContaining("vectorProperty1") + .withMessageContaining("vectorProperty2"); } - } - - @Node - private static class SomeOtherNode { - @Id Long id; - } - - @Node - private static class NodeWithDynamicLabels { - - @Id @GeneratedValue Long id; - - List relatedTo; - - @DynamicLabels List dynamicLabels; - } - - @Node - private static class NodeWithInvalidDynamicLabels { - - @Id @GeneratedValue Long id; - - @DynamicLabels List dynamicLabels; - - @DynamicLabels List moarDynamicLabels; - } - - @Node - private static class ValidInheritedDynamicLabels extends NodeWithDynamicLabels {} - - @Node - private static class InvalidInheritedDynamicLabels extends NodeWithDynamicLabels { - - @DynamicLabels List localDynamicLabels; - } - - @Node - private static class InvalidDynamicLabels { - - @Id @GeneratedValue Long id; - - @DynamicLabels String dynamicLabels; - } - - @Node - private static class CorrectEntity1 { - - @Id private Long id; - - private String name; - - private Map dynamicRelationships; - } - - @Node - private static class CorrectEntity2 { - - @Id private Long id; - - private String name; - - @Relationship(direction = Relationship.Direction.INCOMING) private Map dynamicRelationships; - } - - @Node - private static class MixedDynamicAndExplicitRelationship1 { - - @Id private Long id; - - private String name; - - @Relationship(type = "BAMM") private Map dynamicRelationships; - } - - @Node - private static class MixedDynamicAndExplicitRelationship2 { - - @Id private Long id; - - private String name; - - @Relationship(type = "BAMM", - direction = Relationship.Direction.INCOMING) private Map> dynamicRelationships; - } - - @Node - private static class EntityWithDuplicatedProperties { - - @Id private Long id; - - private String name; - - @Property("name") private String alsoName; - } - - @Node - private static class EntityWithMultipleDuplicatedProperties { - - @Id private Long id; - - private String name; - - @Property("name") private String alsoName; - - @Property("foo") private String somethingElse; - - @Property("foo") private String thisToo; - } - - private abstract static class BaseClassWithPrivatePropertyUnsafe { - - @Id @GeneratedValue - private Long id; - - private String name; - } - - @Node - private static class EntityWithInheritedMultipleDuplicatedProperties extends BaseClassWithPrivatePropertyUnsafe { - - private String name; - } - - private abstract static class BaseClassWithPrivatePropertySafe { - - @Id @GeneratedValue - private Long id; - - private @Transient String name; - } - - @Node - private static class EntityWithNotInheritedTransientProperties extends BaseClassWithPrivatePropertySafe { - - private String name; - } - - @Node("a") - private static class EntityWithSingleLabel { - @Id private Long id; - } - - @Node({ "a", "b", "c" }) - private static class EntityWithMultipleLabels { - @Id private Long id; - } - - @Node(primaryLabel = "a") - private static class EntityWithExplicitPrimaryLabel { - @Id private Long id; - } - - @Node(primaryLabel = "a", labels = { "b", "c" }) - private static class EntityWithExplicitPrimaryLabelAndAdditionalLabels { - @Id private Long id; - } - - @Node(primaryLabel = "Base", labels = { "Bases" }) - private static abstract class BaseClass { - @Id private Long id; - } - - @Node(primaryLabel = "Child", labels = { "Person" }) - private static class Child extends BaseClass { - private String name; - } - - @Node - static class TypeWithInvalidDynamicRelationshipMappings1 { - - @Id private String id; - - private Map bikes1; - - private Map bikes2; - } - - @Node - static class TypeWithInvalidDynamicRelationshipMappings2 { - - @Id private String id; - - private Map bikes1; - - private Map> bikes2; - } - - @Node - static class TypeWithInvalidDynamicRelationshipMappings3 { - - @Id private String id; - - private Map> bikes1; - - private Map> bikes2; - } - - @Node - static class EntityWithCorrectRelationshipProperties { - @Id private String id; - @Relationship HasTargetNodeRelationshipProperties rel; - } - - @Node - static class EntityWithInCorrectRelationshipProperties { - @Id private String id; - @Relationship HasNoTargetNodeRelationshipProperties rel; - } - - @RelationshipProperties - static class HasTargetNodeRelationshipProperties { - - @RelationshipId - private Long id; - - @TargetNode - EntityWithExplicitPrimaryLabel entity; - } - - @RelationshipProperties - static class HasNoTargetNodeRelationshipProperties { - - @RelationshipId - private Long id; - } - - @Node - static class EntityWithBidirectionalRelationship { - - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - List knows; - - @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) - List knownBy; } - @Node - static class EntityWithBidirectionalRelationshipToOtherEntity { - - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - List knows; - - } - - @Node - static class OtherEntityWithBidirectionalRelationship { - - @Id @GeneratedValue - private Long id; - - @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) - List knownBy; - - } - - @Node - static class EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties { - - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - List knows; - - } - - @Node - static class OtherEntityWithBidirectionalRelationshipWithRelationshipProperties { - - @Id @GeneratedValue - private Long id; - - @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) - List knownBy; - - } - - @RelationshipProperties - static class OtherEntityWithBidirectionalRelationshipWithRelationshipPropertiesProperties { - @RelationshipId - private Long id; - - @TargetNode - OtherEntityWithBidirectionalRelationshipWithRelationshipProperties target; - } - - @RelationshipProperties - static class EntityWithBidirectionalRelationshipWithRelationshipPropertiesProperties { - @RelationshipId - private Long id; - - @TargetNode - EntityWithBidirectionalRelationshipToOtherEntityWithRelationshipProperties target; - } - - @Node - static class EntityWithBidirectionalRelationshipProperties { - - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - List knows; - - @Relationship(type = "KNOWS", direction = Relationship.Direction.INCOMING) - List knownBy; - - } - - @RelationshipProperties - static class BidirectionalRelationshipProperties { - - @RelationshipId - private Long id; - - @TargetNode - EntityWithBidirectionalRelationshipProperties target; - } - - @Node - static class EntityLooksLikeHasObserve { - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - private List knows; - } - - @Node - static class OtherEntityLooksLikeHasObserve { - @Id @GeneratedValue - private Long id; - - @Relationship("KNOWS") - private List knows; - } - - @Node - static class WithAnnotatedProperties { - - @Id @GeneratedValue - private Long id; - - private String defaultProperty; - - @Property - private String defaultAnnotatedProperty; - - @Property(readOnly = true) - private String readOnlyProperty; - - @ReadOnlyProperty - private String usingSpringsAnnotation; - - @SuppressWarnings("DefaultAnnotationParam") - @Property(readOnly = false) - private String writableProperty; - } - - static class WithConvertedProperty { - - @ConvertWith - IWillBeConverted converted; - } - - static class IWillBeConverted { - - } - - @Node - static class VectorValid { - @Id @GeneratedValue - private Long id; - - Vector vectorProperty; - } - - @Node - static class VectorInvalid { - @Id @GeneratedValue - private Long id; - - Vector vectorProperty1; - Vector vectorProperty2; - } } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTests.java similarity index 83% rename from src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTests.java index f7fc86104..29928a19b 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentPropertyTests.java @@ -15,22 +15,22 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Michael J. Simons */ -class DefaultNeo4jPersistentPropertyTest { +class DefaultNeo4jPersistentPropertyTests { @ParameterizedTest @CsvSource({ "aName, A_NAME", "ANumberedNam3, A_NUMBERED_NAM_3", "Foo3Bar, FOO_3_BAR", "Foo3BaR, FOO_3_BA_R", "foo3BaR, FOO_3_BA_R", "🖖someThing, 🖖_SOME_THING", "$someThing, $_SOME_THING", - "$$some33Thing, $_$_SOME_3_3_THING", "🧐someThing✋, 🧐_SOME_THING_✋", }) + "$$some33Thing, $_$_SOME_3_3_THING", "🧐someThing✋, 🧐_SOME_THING_✋" }) void toUpperSnakeCaseShouldWork(String name, String expectedEscapedName) { assertThat(DefaultNeo4jPersistentProperty.deriveRelationshipType(name)).isEqualTo(expectedEscapedName); @@ -39,12 +39,15 @@ class DefaultNeo4jPersistentPropertyTest { @Test void toUpperSnakeCaseShouldDealWithNull() { - assertThatIllegalArgumentException().isThrownBy(() -> DefaultNeo4jPersistentProperty.deriveRelationshipType(null)); + assertThatIllegalArgumentException() + .isThrownBy(() -> DefaultNeo4jPersistentProperty.deriveRelationshipType(null)); } @Test void toUpperSnakeCaseShouldDealWithEmptyString() { - assertThatIllegalArgumentException().isThrownBy(() -> DefaultNeo4jPersistentProperty.deriveRelationshipType("")); + assertThatIllegalArgumentException() + .isThrownBy(() -> DefaultNeo4jPersistentProperty.deriveRelationshipType("")); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTests.java similarity index 62% rename from src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTests.java index 5449e52a3..bb28ea11a 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/IdDescriptionTests.java @@ -15,54 +15,65 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; + import org.springframework.data.neo4j.core.schema.IdGenerator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class IdDescriptionTest { +class IdDescriptionTests { @Test void isAssignedShouldWork() { assertThat(IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "foobar").isAssignedId()).isTrue(); - assertThat(IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "foobar").isExternallyGeneratedId()).isFalse(); - assertThat(IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "foobar").isInternallyGeneratedId()).isFalse(); + assertThat(IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "foobar").isExternallyGeneratedId()) + .isFalse(); + assertThat(IdDescription.forAssignedIds(Constants.NAME_OF_ROOT_NODE, "foobar").isInternallyGeneratedId()) + .isFalse(); } @Test void idIsGeneratedInternallyShouldWork() { assertThat(IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE).isAssignedId()).isFalse(); - assertThat(IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE).isExternallyGeneratedId()).isFalse(); - assertThat(IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE).isInternallyGeneratedId()).isTrue(); + assertThat(IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE).isExternallyGeneratedId()) + .isFalse(); + assertThat(IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE).isInternallyGeneratedId()) + .isTrue(); } @Test void idIsGeneratedExternally() { - assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar").isAssignedId()) - .isFalse(); - assertThat( - IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar").isExternallyGeneratedId()) - .isTrue(); - assertThat( - IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar").isInternallyGeneratedId()) - .isFalse(); + assertThat(IdDescription + .forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar") + .isAssignedId()).isFalse(); + assertThat(IdDescription + .forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar") + .isExternallyGeneratedId()).isTrue(); + assertThat(IdDescription + .forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, DummyIdGenerator.class, null, "foobar") + .isInternallyGeneratedId()).isFalse(); - assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar").isAssignedId()).isFalse(); - assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar").isExternallyGeneratedId()).isTrue(); - assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar").isInternallyGeneratedId()).isFalse(); + assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar") + .isAssignedId()).isFalse(); + assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar") + .isExternallyGeneratedId()).isTrue(); + assertThat(IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, null, "someId", "foobar") + .isInternallyGeneratedId()).isFalse(); } - private static class DummyIdGenerator implements IdGenerator { + private static final class DummyIdGenerator implements IdGenerator { @Override public Void generateId(String primaryLabel, Object entity) { return null; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTests.java similarity index 76% rename from src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTests.java index 6ee81681c..2543ff553 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTests.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,6 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.neo4j.driver.Value; import org.neo4j.driver.internal.value.StringValue; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.annotation.Transient; @@ -79,47 +76,32 @@ import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCus import org.springframework.data.neo4j.test.LogbackCapture; import org.springframework.data.neo4j.test.LogbackCapturingExtension; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * @author Michael J. Simons */ -class Neo4jMappingContextTest { +class Neo4jMappingContextTests { - @ExtendWith(LogbackCapturingExtension.class) - @Nested - class InvalidRelationshipProperties { + private static Set> scanAndShuffle(String basePackage) throws ClassNotFoundException { - @Test // GH-2118 - void startupWithoutInternallyGeneratedIDShouldFail() { + Comparator> pseudoRandomComparator = new Comparator>() { + private final Map uniqueIds = new IdentityHashMap<>(); - Neo4jMappingContext schema = new Neo4jMappingContext(); - assertThatIllegalStateException().isThrownBy(() -> { - schema.setInitialEntitySet(new HashSet<>( - Arrays.asList(IrrelevantSourceContainer.class, InvalidRelationshipPropertyContainer.class, - IrrelevantTargetContainer.class))); - schema.initialize(); - }).withMessage("The class `org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTest$InvalidRelationshipPropertyContainer` for the properties of a relationship is missing a property for the generated, internal ID (`@Id @GeneratedValue Long id` or `@Id @GeneratedValue String id`) which is needed for safely updating properties"); - } + @Override + public int compare(Class o1, Class o2) { + UUID e1 = this.uniqueIds.computeIfAbsent(o1, k -> UUID.randomUUID()); + UUID e2 = this.uniqueIds.computeIfAbsent(o2, k -> UUID.randomUUID()); + return e1.compareTo(e2); + } + }; - @Test // GH-2214 - void startupWithWrongKindOfGeneratedIDShouldFail() { - - Neo4jMappingContext schema = new Neo4jMappingContext(); - assertThatIllegalStateException().isThrownBy(() -> { - schema.setInitialEntitySet(new HashSet<>( - Arrays.asList(IrrelevantSourceContainer3.class, InvalidRelationshipPropertyContainer2.class, - IrrelevantTargetContainer.class))); - schema.initialize(); - }).withMessage("The class `org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTest$InvalidRelationshipPropertyContainer2` for the properties of a relationship is missing a property for the generated, internal ID (`@Id @GeneratedValue Long id` or `@Id @GeneratedValue String id`) which is needed for safely updating properties"); - } - - @Test // GH-2118 - void noWarningShouldBeLogged(LogbackCapture logbackCapture) { - - Neo4jMappingContext schema = new Neo4jMappingContext(); - schema.setInitialEntitySet(new HashSet<>(Arrays.asList(IrrelevantSourceContainer2.class, FriendshipRelationship.class, IrrelevantTargetContainer2.class))); - schema.initialize(); - assertThat(logbackCapture.getFormattedMessages()).isEmpty(); - } + Set> scanResult = Neo4jEntityScanner.get().scan(basePackage); + Set> initialEntities = new TreeSet<>(pseudoRandomComparator); + initialEntities.addAll(scanResult); + return initialEntities; } @Test @@ -136,15 +118,16 @@ class Neo4jMappingContextTest { assertThat(description.getIdDescription().isInternallyGeneratedId()).isTrue(); assertThat(description.getGraphProperties()).extracting(GraphPropertyDescription::getFieldName) - .containsExactlyInAnyOrder("id", "name", "first_name"); + .containsExactlyInAnyOrder("id", "name", "first_name"); assertThat(description.getGraphProperties()).extracting(GraphPropertyDescription::getPropertyName) - .containsExactlyInAnyOrder("id", "name", "firstName"); + .containsExactlyInAnyOrder("id", "name", "firstName"); - Collection expectedRelationships = Arrays.asList("[:OWNS] -> (:BikeNode)", "[:THE_SUPER_BIKE] -> (:BikeNode)"); + Collection expectedRelationships = Arrays.asList("[:OWNS] -> (:BikeNode)", + "[:THE_SUPER_BIKE] -> (:BikeNode)"); Collection relationships = description.getRelationships(); assertThat(relationships.stream().filter(r -> !r.isDynamic())).allMatch(d -> expectedRelationships - .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget().getPrimaryLabel()))); + .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget().getPrimaryLabel()))); }); NodeDescription optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); @@ -156,7 +139,7 @@ class Neo4jMappingContextTest { Collection expectedRelationships = Arrays.asList("[:OWNER] -> (:User)", "[:RENTER] -> (:User)"); Collection relationships = description.getRelationships(); assertThat(relationships.stream().filter(r -> !r.isDynamic())).allMatch(d -> expectedRelationships - .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget().getPrimaryLabel()))); + .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget().getPrimaryLabel()))); }); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); @@ -176,7 +159,8 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidId.class))); assertThatIllegalArgumentException().isThrownBy(() -> schema.initialize()) - .withMessageMatching("Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); + .withMessageMatching( + "Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); } @Test @@ -185,7 +169,7 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidIdType.class))); assertThatIllegalArgumentException().isThrownBy(schema::initialize) - .withMessageMatching("Internally generated ids can only be assigned to one of .*"); + .withMessageMatching("Internally generated ids can only be assigned to one of .*"); } @Test @@ -193,7 +177,7 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); assertThatIllegalStateException().isThrownBy(() -> schema.getPersistentEntity(MissingId.class)) - .withMessage("Missing id property on " + MissingId.class); + .withMessage("Missing id property on " + MissingId.class); } @Test @@ -202,8 +186,8 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); bikeNodeEntity.doWithAssociations((Association association) -> Assertions - .assertThat(schema.getRequiredMappingFunctionFor(association.getInverse().getAssociationTargetType())) - .isNotNull()); + .assertThat(schema.getRequiredMappingFunctionFor(association.getInverse().getAssociationTargetType())) + .isNotNull()); } @Test @@ -212,11 +196,11 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); assertThat(bikeNodeEntity.getRequiredPersistentProperty("renter").getAssociation()).isNotNull() - .satisfies(association -> { - assertThat(association).isInstanceOf(RelationshipDescription.class); - RelationshipDescription relationshipDescription = (RelationshipDescription) association; - assertThat(relationshipDescription.getType()).isEqualTo("RENTER"); - }); + .satisfies(association -> { + assertThat(association).isInstanceOf(RelationshipDescription.class); + RelationshipDescription relationshipDescription = (RelationshipDescription) association; + assertThat(relationshipDescription.getType()).isEqualTo("RENTER"); + }); } @Test @@ -233,6 +217,7 @@ class Neo4jMappingContextTest { void complexPropertyWithConverterShouldNotBeConsideredAsAssociation() { class ConvertibleTypeConverter implements GenericConverter { + @Override public Set getConvertibleTypes() { // in the real world this should also define the opposite way @@ -244,6 +229,7 @@ class Neo4jMappingContextTest { // no implementation needed for this test return null; } + } Neo4jMappingContext schema = new Neo4jMappingContext( @@ -285,7 +271,8 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity enumRelNodeEntity = schema.getPersistentEntity(EnumRelNode.class); List associations = new ArrayList<>(); - enumRelNodeEntity.doWithAssociations((Association a) -> associations.add(a.getInverse())); + enumRelNodeEntity + .doWithAssociations((Association a) -> associations.add(a.getInverse())); assertThat(associations).hasSize(2); } @@ -297,9 +284,9 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity entity = schema.getPersistentEntity(WithInvalidCompositeUsage.class); Neo4jPersistentProperty property = entity.getRequiredPersistentProperty("doesntWorkOnScalar"); - assertThatIllegalArgumentException() - .isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) - .withMessageMatching("@CompositeProperty can only be used on Map properties without additional configuration. Was used on `.*` in `.*`"); + assertThatIllegalArgumentException().isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) + .withMessageMatching( + "@CompositeProperty can only be used on Map properties without additional configuration. Was used on `.*` in `.*`"); } @Test @@ -317,9 +304,9 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity entity = schema.getPersistentEntity(WithInvalidCompositeUsage.class); Neo4jPersistentProperty property = entity.getRequiredPersistentProperty("doesntWorkOnCollection"); - assertThatIllegalArgumentException() - .isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) - .withMessageMatching("@CompositeProperty can only be used on Map properties without additional configuration. Was used on `.*` in `.*`"); + assertThatIllegalArgumentException().isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) + .withMessageMatching( + "@CompositeProperty can only be used on Map properties without additional configuration. Was used on `.*` in `.*`"); } @Test @@ -328,9 +315,9 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity entity = schema.getPersistentEntity(WithInvalidCompositeUsage.class); Neo4jPersistentProperty property = entity.getRequiredPersistentProperty("mismatch"); - assertThatIllegalArgumentException() - .isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) - .withMessageMatching("The property type `.*` created by `.*` used on `.*` in `.*` doesn't match the actual property type"); + assertThatIllegalArgumentException().isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) + .withMessageMatching( + "The property type `.*` created by `.*` used on `.*` in `.*` doesn't match the actual property type"); } @Test @@ -339,9 +326,9 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity entity = schema.getPersistentEntity(WithInvalidCompositeUsage.class); Neo4jPersistentProperty property = entity.getRequiredPersistentProperty("doesntWorkOnWrongMapType"); - assertThatIllegalArgumentException() - .isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) - .withMessageMatching("@CompositeProperty can only be used on Map properties with a key type of String or enum. Was used on `.*` in `.*`"); + assertThatIllegalArgumentException().isThrownBy(() -> schema.getOptionalCustomConversionsFor(property)) + .withMessageMatching( + "@CompositeProperty can only be used on Map properties with a key type of String or enum. Was used on `.*` in `.*`"); } @Test // DATAGRAPH-1446 @@ -352,14 +339,12 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity entity = schema.getPersistentEntity(P.class); assertThat( entity.getRelationships().stream().sorted(Comparator.comparing(RelationshipDescription::getFieldName))) - .hasSize(2) - .satisfies(l -> assertThat(l) - .extracting(RelationshipDescription::getRelationshipPropertiesEntity) - .extracting(e -> (Class) e.getUnderlyingClass()) - .containsExactly(R1.class, R2.class) - ) - .extracting(r -> (Class) r.getTarget().getUnderlyingClass()) - .containsExactly(B.class, C.class); + .hasSize(2) + .satisfies(l -> assertThat(l).extracting(RelationshipDescription::getRelationshipPropertiesEntity) + .extracting(e -> (Class) e.getUnderlyingClass()) + .containsExactly(R1.class, R2.class)) + .extracting(r -> (Class) r.getTarget().getUnderlyingClass()) + .containsExactly(B.class, C.class); } @Test // DATAGRAPH-1448 @@ -394,8 +379,9 @@ class Neo4jMappingContextTest { } @ParameterizedTest // DATAGRAPH-1459 - @ValueSource(classes = {InvalidMultiDynamics1.class, InvalidMultiDynamics2.class, InvalidMultiDynamics3.class, InvalidMultiDynamics4.class}) - void shouldDetectAllVariantsOfMultipleDynamicRelationships(Class thingWithRelations) { + @ValueSource(classes = { InvalidMultiDynamics1.class, InvalidMultiDynamics2.class, InvalidMultiDynamics3.class, + InvalidMultiDynamics4.class }) + void shouldDetectAllVariantsOfMultipleDynamicRelationships(Class thingWithRelations) { assertThatIllegalStateException().isThrownBy(() -> { Neo4jMappingContext schema = new Neo4jMappingContext(); @@ -405,13 +391,14 @@ class Neo4jMappingContextTest { } @ParameterizedTest // GH-2201 - @ValueSource(booleans = {true, false}) + @ValueSource(booleans = { true, false }) void useOfInterfaceAndImplementationShouldWork(boolean explicit) { List>> listOfInitialEntities = new ArrayList<>(); if (!explicit) { listOfInitialEntities.add(Collections.emptySet()); - } else { + } + else { listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterface.class, SomeInterfaceImpl.class))); listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl.class, SomeInterface.class))); } @@ -435,14 +422,14 @@ class Neo4jMappingContextTest { } @ParameterizedTest // GH-2201 - @ValueSource(booleans = {true, false}) + @ValueSource(booleans = { true, false }) void useOfAnnotatedInterfaceAndImplementationShouldWork(boolean explicit) { - List>> listOfInitialEntities = new ArrayList<>(); if (!explicit) { listOfInitialEntities.add(Collections.emptySet()); - } else { + } + else { listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterface2.class, SomeInterfaceImpl2.class))); listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl2.class, SomeInterface2.class))); } @@ -467,19 +454,26 @@ class Neo4jMappingContextTest { } @ParameterizedTest // GH-2201 - @ValueSource(booleans = {true, false}) + @ValueSource(booleans = { true, false }) void differentImplementationsForAnInterfaceShouldWork(boolean explicit) { List>> listOfInitialEntities = new ArrayList<>(); if (!explicit) { listOfInitialEntities.add(Collections.emptySet()); - } else { - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterface3.class, SomeInterfaceImpl3a.class, SomeInterfaceImpl3b.class))); - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterface3.class, SomeInterfaceImpl3b.class, SomeInterfaceImpl3a.class))); - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl3a.class, SomeInterface3.class, SomeInterfaceImpl3b.class))); - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl3a.class, SomeInterfaceImpl3b.class, SomeInterface3.class))); - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl3b.class, SomeInterface3.class, SomeInterfaceImpl3a.class))); - listOfInitialEntities.add(new HashSet<>(Arrays.asList(SomeInterfaceImpl3b.class, SomeInterfaceImpl3a.class, SomeInterface3.class))); + } + else { + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterface3.class, SomeInterfaceImpl3a.class, SomeInterfaceImpl3b.class))); + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterface3.class, SomeInterfaceImpl3b.class, SomeInterfaceImpl3a.class))); + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterfaceImpl3a.class, SomeInterface3.class, SomeInterfaceImpl3b.class))); + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterfaceImpl3a.class, SomeInterfaceImpl3b.class, SomeInterface3.class))); + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterfaceImpl3b.class, SomeInterface3.class, SomeInterfaceImpl3a.class))); + listOfInitialEntities.add(new HashSet<>( + Arrays.asList(SomeInterfaceImpl3b.class, SomeInterfaceImpl3a.class, SomeInterface3.class))); } for (Set> initialEntities : listOfInitialEntities) { @@ -505,7 +499,8 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity entity = schema.getPersistentEntity(UserNode.class); - assertThat(entity.getRelationships()).anyMatch(r -> r.getFieldName().equals("theSuperBike") && r.getType().equals("THE_SUPER_BIKE")); + assertThat(entity.getRelationships()) + .anyMatch(r -> r.getFieldName().equals("theSuperBike") && r.getType().equals("THE_SUPER_BIKE")); } @Test // COMMONS-2390 @@ -545,16 +540,16 @@ class Neo4jMappingContextTest { void shouldFindPostLoadMethods() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); Neo4jPersistentEntity entity = neo4jMappingContext.getPersistentEntity(EntityWithPostLoadMethods.class); - assertThat(neo4jMappingContext.getPostLoadMethods(entity)) - .hasSize(2) - .extracting(Neo4jMappingContext.MethodHolder::getName) - .containsExactlyInAnyOrder("m1", "m2"); + assertThat(neo4jMappingContext.getPostLoadMethods(entity)).hasSize(2) + .extracting(Neo4jMappingContext.MethodHolder::getName) + .containsExactlyInAnyOrder("m1", "m2"); } @Test // GH-2499 void shouldInvokePostLoad() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity entity = (Neo4jPersistentEntity) neo4jMappingContext.getPersistentEntity(EntityWithPostLoadMethods.class); + Neo4jPersistentEntity entity = (Neo4jPersistentEntity) neo4jMappingContext + .getPersistentEntity(EntityWithPostLoadMethods.class); EntityWithPostLoadMethods instance = new EntityWithPostLoadMethods(); neo4jMappingContext.invokePostLoad(entity, instance); assertThat(instance.m1).isEqualTo("setM1"); @@ -564,61 +559,41 @@ class Neo4jMappingContextTest { @Test // GH-2499 void shouldFindPostLoadMethodsWithInheritance() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity entity = neo4jMappingContext.getPersistentEntity(EntityWithWithMorePostLoadMethods.class); - assertThat(neo4jMappingContext.getPostLoadMethods(entity)) - .hasSize(3) - .extracting(Neo4jMappingContext.MethodHolder::getName) - .containsExactlyInAnyOrder("m1", "m2", "m2a"); + Neo4jPersistentEntity entity = neo4jMappingContext + .getPersistentEntity(EntityWithWithMorePostLoadMethods.class); + assertThat(neo4jMappingContext.getPostLoadMethods(entity)).hasSize(3) + .extracting(Neo4jMappingContext.MethodHolder::getName) + .containsExactlyInAnyOrder("m1", "m2", "m2a"); } @Test // GH-2499 void shouldFindPostLoadMethodsInKotlinClasses() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); Neo4jPersistentEntity entity = neo4jMappingContext.getPersistentEntity(KotlinBaseImpl.class); - assertThat(neo4jMappingContext.getPostLoadMethods(entity)) - .hasSize(1) - .extracting(Neo4jMappingContext.MethodHolder::getName) - .containsExactlyInAnyOrder("bar"); + assertThat(neo4jMappingContext.getPostLoadMethods(entity)).hasSize(1) + .extracting(Neo4jMappingContext.MethodHolder::getName) + .containsExactlyInAnyOrder("bar"); } @Test // GH-2499 void shouldFindPostLoadMethodsInKotlinClassesByDelegate() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); Neo4jPersistentEntity entity = neo4jMappingContext.getPersistentEntity(KotlinAImpl.class); - assertThat(neo4jMappingContext.getPostLoadMethods(entity)) - .hasSize(1) - .extracting(Neo4jMappingContext.MethodHolder::getName) - .containsExactlyInAnyOrder("bar"); + assertThat(neo4jMappingContext.getPostLoadMethods(entity)).hasSize(1) + .extracting(Neo4jMappingContext.MethodHolder::getName) + .containsExactlyInAnyOrder("bar"); } @Test // GH-2499 void shouldInvokePostLoadInKotlinClassesByDelegate() { Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - Neo4jPersistentEntity entity = (Neo4jPersistentEntity) neo4jMappingContext.getPersistentEntity(KotlinAImpl.class); + Neo4jPersistentEntity entity = (Neo4jPersistentEntity) neo4jMappingContext + .getPersistentEntity(KotlinAImpl.class); KotlinAImpl instance = new KotlinAImpl(); neo4jMappingContext.invokePostLoad(entity, instance); assertThat(instance.getBaseName()).isEqualTo("someValue"); } - private static Set> scanAndShuffle(String basePackage) throws ClassNotFoundException { - - Comparator> pseudoRandomComparator = new Comparator>() { - private final Map uniqueIds = new IdentityHashMap<>(); - - @Override - public int compare(Class o1, Class o2) { - UUID e1 = uniqueIds.computeIfAbsent(o1, k -> UUID.randomUUID()); - UUID e2 = uniqueIds.computeIfAbsent(o2, k -> UUID.randomUUID()); - return e1.compareTo(e2); - } - }; - - Set> scanResult = Neo4jEntityScanner.get().scan(basePackage); - Set> initialEntities = new TreeSet<>(pseudoRandomComparator); - initialEntities.addAll(scanResult); - return initialEntities; - } - @RepeatedTest(10) // GH-2574 void hierarchyMustBeConsistentlyReportedWithIntermediateConcreteClasses() throws ClassNotFoundException { @@ -629,9 +604,10 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity b1 = Objects.requireNonNull(neo4jMappingContext.getPersistentEntity(Model.B1.class)); List children = b1.getChildNodeDescriptionsInHierarchy() - .stream().map(NodeDescription::getPrimaryLabel) - .sorted() - .collect(Collectors.toList()); + .stream() + .map(NodeDescription::getPrimaryLabel) + .sorted() + .collect(Collectors.toList()); assertThat(children).containsExactly("B2", "B2a", "B3", "B3a"); } @@ -639,28 +615,33 @@ class Neo4jMappingContextTest { @Test void characteristicsShouldBeApplied() { - Neo4jMappingContext neo4jMappingContext1 = Neo4jMappingContext.builder().withPersistentPropertyCharacteristicsProvider((property, owner) -> { - if (owner.getUnderlyingClass().equals(UserNode.class)) { - if (property.getName().equals("name")) { - return PersistentPropertyCharacteristics.treatAsTransient(); - } else if (property.getName().equals("first_name")) { - return PersistentPropertyCharacteristics.treatAsReadOnly(); + Neo4jMappingContext neo4jMappingContext1 = Neo4jMappingContext.builder() + .withPersistentPropertyCharacteristicsProvider((property, owner) -> { + if (owner.getUnderlyingClass().equals(UserNode.class)) { + if (property.getName().equals("name")) { + return PersistentPropertyCharacteristics.treatAsTransient(); + } + else if (property.getName().equals("first_name")) { + return PersistentPropertyCharacteristics.treatAsReadOnly(); + } + } + if (property.getType().equals(ConvertibleType.class)) { + return PersistentPropertyCharacteristics.treatAsTransient(); } - } - if (property.getType().equals(ConvertibleType.class)) { - return PersistentPropertyCharacteristics.treatAsTransient(); - } - return PersistentPropertyCharacteristics.useDefaults(); - }).build(); + return PersistentPropertyCharacteristics.useDefaults(); + }) + .build(); Neo4jMappingContext neo4jMappingContext2 = Neo4jMappingContext.builder().build(); Neo4jPersistentEntity userEntity = neo4jMappingContext1.getPersistentEntity(UserNode.class); - assertThat(userEntity.getPersistentProperty("name")).isNull(); // Transient properties won't materialize + // Transient properties won't materialize + assertThat(userEntity.getPersistentProperty("name")).isNull(); assertThat(userEntity.getRequiredPersistentProperty("first_name").isTransient()).isFalse(); assertThat(userEntity.getRequiredPersistentProperty("first_name").isReadOnly()).isTrue(); - Neo4jPersistentEntity entityWithConvertible = neo4jMappingContext1.getPersistentEntity(EntityWithConvertibleProperty.class); + Neo4jPersistentEntity entityWithConvertible = neo4jMappingContext1 + .getPersistentEntity(EntityWithConvertibleProperty.class); assertThat(entityWithConvertible.getPersistentProperty("convertibleType")).isNull(); entityWithConvertible = neo4jMappingContext2.getPersistentEntity(EntityWithConvertibleProperty.class); @@ -677,9 +658,10 @@ class Neo4jMappingContextTest { Neo4jPersistentEntity a1 = Objects.requireNonNull(neo4jMappingContext.getPersistentEntity(Model.A1.class)); List children = a1.getChildNodeDescriptionsInHierarchy() - .stream().map(NodeDescription::getPrimaryLabel) - .sorted() - .collect(Collectors.toList()); + .stream() + .map(NodeDescription::getPrimaryLabel) + .sorted() + .collect(Collectors.toList()); assertThat(children).containsExactly("A2", "A3", "A4"); } @@ -694,25 +676,46 @@ class Neo4jMappingContextTest { }); } + enum A { + + A1, A2 + + } + + enum ExtendedA { + + EA1, EA2 { + @Override + public void doNothing() { + } + }; + + @SuppressWarnings("unused") + void doNothing() { + + } + + } + + interface SomeInterface { + + } + + @Node("A") + interface SomeInterface2 { + + } + + interface SomeInterface3 { + + } + static class EntityWithPostLoadMethods { String m1; String m2; - void m0() { - } - - @PostLoad - void m1() { - m1 = "setM1"; - } - - @PostLoad - public void m2() { - m2 = "setM2"; - } - @PostLoad static void m3() { @@ -727,13 +730,28 @@ class Neo4jMappingContextTest { static int m5() { return 1; } + + void m0() { + } + + @PostLoad + void m1() { + this.m1 = "setM1"; + } + + @PostLoad + void m2() { + this.m2 = "setM2"; + } + } static class EntityWithWithMorePostLoadMethods extends EntityWithPostLoadMethods { @PostLoad - public void m2a() { + void m2a() { } + } static class DummyIdGenerator implements IdGenerator { @@ -742,31 +760,40 @@ class Neo4jMappingContextTest { public Void generateId(String primaryLabel, Object entity) { return null; } + } @Node("User") static class UserNode { - @org.springframework.data.annotation.Id @GeneratedValue @SuppressWarnings("unused") - private long id; - - @Relationship(type = "OWNS") @SuppressWarnings("unused") + @Relationship(type = "OWNS") + @SuppressWarnings("unused") List bikes; - @Relationship @SuppressWarnings("unused") + @Relationship + @SuppressWarnings("unused") BikeNode theSuperBike; @SuppressWarnings("unused") String name; - @Transient @SuppressWarnings("unused") + @Transient + @SuppressWarnings("unused") String anAnnotatedTransientProperty; - @Transient @SuppressWarnings("unused") + @Transient + @SuppressWarnings("unused") List someOtherTransientThings; - @Property(name = "firstName") @SuppressWarnings("unused") + @Property(name = "firstName") + @SuppressWarnings("unused") String first_name; + + @org.springframework.data.annotation.Id + @GeneratedValue + @SuppressWarnings("unused") + private long id; + } @Node @@ -774,29 +801,9 @@ class Neo4jMappingContextTest { } - enum A { - A1, A2 - } - - enum ExtendedA { - - EA1, EA2 { - @Override - public void doNothing() {} - }; - - @SuppressWarnings("unused") - public void doNothing() { - - } - } - @Node static class BikeNode { - @Id @SuppressWarnings("unused") - private String id; - @SuppressWarnings("unused") UserNode owner; @@ -817,36 +824,42 @@ class Neo4jMappingContextTest { @SuppressWarnings("unused") Map funnyDynamicProperties; + + @Id + @SuppressWarnings("unused") + private String id; + } @Node static class EnumRelNode { - @Id @SuppressWarnings("unused") - private String id; - @SuppressWarnings("unused") Map relA; @SuppressWarnings("unused") Map relEA; + + @Id + @SuppressWarnings("unused") + private String id; + } @Node static class TripNode { - @Id @SuppressWarnings("unused") + String name; + + @Id + @SuppressWarnings("unused") private String id; - String name; } @Node static class InvalidMultiDynamics1 { - @Id @SuppressWarnings("unused") - private String id; - @SuppressWarnings("unused") String name; @@ -855,236 +868,305 @@ class Neo4jMappingContextTest { @SuppressWarnings("unused") Map relEB; + + @Id + @SuppressWarnings("unused") + private String id; + } @Node static class InvalidMultiDynamics2 { - @Id @SuppressWarnings("unused") - private String id; - @SuppressWarnings("unused") String name; @SuppressWarnings("unused") Map relEA; - @Relationship @SuppressWarnings("unused") + @Relationship + @SuppressWarnings("unused") Map relEB; + + @Id + @SuppressWarnings("unused") + private String id; + } @Node static class InvalidMultiDynamics3 { - @Id @SuppressWarnings("unused") - private String id; - String name; - @Relationship @SuppressWarnings("unused") + @Relationship + @SuppressWarnings("unused") Map relEA; - @Relationship @SuppressWarnings("unused") + @Relationship + @SuppressWarnings("unused") Map relEB; + + @Id + @SuppressWarnings("unused") + private String id; + } @Node static class InvalidMultiDynamics4 { - @Id @SuppressWarnings("unused") - private String id; - @SuppressWarnings("unused") String name; - @Relationship @SuppressWarnings("unused") + @Relationship + @SuppressWarnings("unused") Map relEA; @SuppressWarnings("unused") Map relEB; - } + @Id + @SuppressWarnings("unused") + private String id; + + } @Node static class InvalidId { - @Id @GeneratedValue @Property("getMappingFunctionFor") @SuppressWarnings("unused") + @Id + @GeneratedValue + @Property("getMappingFunctionFor") + @SuppressWarnings("unused") private String id; + } @Node static class InvalidIdType { - @Id @GeneratedValue @SuppressWarnings("unused") + @Id + @GeneratedValue + @SuppressWarnings("unused") private Double id; + } @Node - static class MissingId {} + static class MissingId { + + } @Node static class EntityWithConvertibleProperty { - @Id @GeneratedValue @SuppressWarnings("unused") + @Id + @GeneratedValue + @SuppressWarnings("unused") private Long id; private ConvertibleType convertibleType; + } - static class ConvertibleType {} + static class ConvertibleType { + + } @Node static class WithInvalidCompositeUsage { - @Id @GeneratedValue private Long id; - - @CompositeProperty @SuppressWarnings("unused") + @CompositeProperty + @SuppressWarnings("unused") String doesntWorkOnScalar; - @CompositeProperty @SuppressWarnings("unused") + @CompositeProperty + @SuppressWarnings("unused") Map doesntWorkOnWrongMapType; - @CompositeProperty @SuppressWarnings("unused") + @CompositeProperty + @SuppressWarnings("unused") List doesntWorkOnCollection; - @CompositeProperty(converter = MissingIdToMapConverter.class) @SuppressWarnings("unused") + @CompositeProperty(converter = MissingIdToMapConverter.class) + @SuppressWarnings("unused") String mismatch; + + @Id + @GeneratedValue + private Long id; + } @Node static class WithValidCompositeUsage { - @Id @GeneratedValue @SuppressWarnings("unused") + @CompositeProperty(converter = MissingIdToMapConverter.class) + @SuppressWarnings("unused") + MissingId worksWithExplictConverter; + + @Id + @GeneratedValue + @SuppressWarnings("unused") private Long id; - @CompositeProperty(converter = MissingIdToMapConverter.class) @SuppressWarnings("unused") - MissingId worksWithExplictConverter; } @Node static class IrrelevantSourceContainer { - @Id @GeneratedValue @SuppressWarnings("unused") + + @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") + @SuppressWarnings("unused") + InvalidRelationshipPropertyContainer relationshipPropertyContainer; + + @Id + @GeneratedValue + @SuppressWarnings("unused") private Long id; - @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") @SuppressWarnings("unused") - InvalidRelationshipPropertyContainer relationshipPropertyContainer; } @RelationshipProperties static class InvalidRelationshipPropertyContainer { - @TargetNode @SuppressWarnings("unused") + + @TargetNode + @SuppressWarnings("unused") private IrrelevantTargetContainer irrelevantTargetContainer; + } @Node static class IrrelevantSourceContainer3 { - @Id @GeneratedValue - private Long id; @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") InvalidRelationshipPropertyContainer2 relationshipPropertyContainer; + + @Id + @GeneratedValue + private Long id; + } @RelationshipProperties static class InvalidRelationshipPropertyContainer2 { - @Id @GeneratedValue(GeneratedValue.UUIDGenerator.class) + @Id + @GeneratedValue(GeneratedValue.UUIDGenerator.class) private UUID id; @TargetNode private IrrelevantTargetContainer irrelevantTargetContainer; + } @Node static class IrrelevantTargetContainer { - @Id @GeneratedValue @SuppressWarnings("unused") + + @Id + @GeneratedValue + @SuppressWarnings("unused") private Long id; + } @Node static class IrrelevantSourceContainer2 { - @Id @GeneratedValue @SuppressWarnings("unused") - private Long id; @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") @SuppressWarnings("unused") List relationshipPropertyContainer; + + @Id + @GeneratedValue + @SuppressWarnings("unused") + private Long id; + } @RelationshipProperties static class RelationshipPropertyContainer { - @RelationshipId @SuppressWarnings("unused") + + @RelationshipId + @SuppressWarnings("unused") private Long id; @TargetNode @SuppressWarnings("unused") private IrrelevantTargetContainer irrelevantTargetContainer; + } @Node static class IrrelevantTargetContainer2 { - @Id @GeneratedValue @SuppressWarnings("unused") - private Long id; - } - interface SomeInterface { + @Id + @GeneratedValue + @SuppressWarnings("unused") + private Long id; + } @Node("SomeInterface") static class SomeInterfaceImpl implements SomeInterface { - @Id @GeneratedValue @SuppressWarnings("unused") - private Long id; @SuppressWarnings("unused") SomeInterface related; - } - @Node("A") - interface SomeInterface2 { + @Id + @GeneratedValue + @SuppressWarnings("unused") + private Long id; + } static class SomeInterfaceImpl2 implements SomeInterface2 { - @Id @GeneratedValue @SuppressWarnings("unused") - private Long id; @SuppressWarnings("unused") SomeInterface2 related; + + @Id + @GeneratedValue + @SuppressWarnings("unused") + private Long id; + } - interface SomeInterface3 { - } - - @Node({"SomeInterface3a"}) + @Node({ "SomeInterface3a" }) static class SomeInterfaceImpl3a implements SomeInterface3 { + @SuppressWarnings("unused") + SomeInterface3 related; + @Id @GeneratedValue @SuppressWarnings("unused") private Long id; - @SuppressWarnings("unused") - SomeInterface3 related; } - @Node({"SomeInterface3b"}) + @Node({ "SomeInterface3b" }) static class SomeInterfaceImpl3b implements SomeInterface3 { + @SuppressWarnings("unused") + SomeInterface3 related; + @Id @GeneratedValue @SuppressWarnings("unused") private Long id; - @SuppressWarnings("unused") - SomeInterface3 related; } @Node public static class SomeBaseEntity { + @Id @GeneratedValue public Long internalId; public String id; + } @Node @@ -1092,12 +1174,13 @@ class Neo4jMappingContextTest { @Relationship("A") public Set as; + @Relationship("B") public Set bs; + @Relationship("C") public Set cs; - } public static class RelationshipPropertiesBaseClass { @@ -1109,9 +1192,11 @@ class Neo4jMappingContextTest { @TargetNode public T target; + } - public static abstract class RelationshipPropertiesAbstractClass extends RelationshipPropertiesBaseClass { + public abstract static class RelationshipPropertiesAbstractClass + extends RelationshipPropertiesBaseClass { } @@ -1132,12 +1217,58 @@ class Neo4jMappingContextTest { static class MissingIdToMapConverter implements Neo4jPersistentPropertyToMapConverter { - @Override public Map decompose(MissingId property, Neo4jConversionService conversionService) { + @Override + public Map decompose(MissingId property, Neo4jConversionService conversionService) { return null; } - @Override public MissingId compose(Map source, Neo4jConversionService conversionService) { + @Override + public MissingId compose(Map source, Neo4jConversionService conversionService) { return null; } + } + + @ExtendWith(LogbackCapturingExtension.class) + @Nested + class InvalidRelationshipProperties { + + @Test // GH-2118 + void startupWithoutInternallyGeneratedIDShouldFail() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + assertThatIllegalStateException().isThrownBy(() -> { + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(IrrelevantSourceContainer.class, + InvalidRelationshipPropertyContainer.class, IrrelevantTargetContainer.class))); + schema.initialize(); + }) + .withMessage( + "The class `org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTests$InvalidRelationshipPropertyContainer` for the properties of a relationship is missing a property for the generated, internal ID (`@Id @GeneratedValue Long id` or `@Id @GeneratedValue String id`) which is needed for safely updating properties"); + } + + @Test // GH-2214 + void startupWithWrongKindOfGeneratedIDShouldFail() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + assertThatIllegalStateException().isThrownBy(() -> { + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(IrrelevantSourceContainer3.class, + InvalidRelationshipPropertyContainer2.class, IrrelevantTargetContainer.class))); + schema.initialize(); + }) + .withMessage( + "The class `org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTests$InvalidRelationshipPropertyContainer2` for the properties of a relationship is missing a property for the generated, internal ID (`@Id @GeneratedValue Long id` or `@Id @GeneratedValue String id`) which is needed for safely updating properties"); + } + + @Test // GH-2118 + void noWarningShouldBeLogged(LogbackCapture logbackCapture) { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(IrrelevantSourceContainer2.class, + FriendshipRelationship.class, IrrelevantTargetContainer2.class))); + schema.initialize(); + assertThat(logbackCapture.getFormattedMessages()).isEmpty(); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTests.java similarity index 94% rename from src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTests.java index 7ee606e88..f34fa5dba 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyFilterTests.java @@ -15,24 +15,27 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.neo4j.integration.shared.common.PersonWithRelationship; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class PropertyFilterTest { +class PropertyFilterTests { @ParameterizedTest @CsvSource({ "id, foo", "hobbies, foo", "hobbies.name, hobbies.foo" }) void toDotPathShouldWork(String value, String newDotPath) { - PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(PersonWithRelationship.class).append(value); + PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath + .withRootType(PersonWithRelationship.class) + .append(value); String dotPath; dotPath = PropertyFilter.toDotPath(path, null); assertThat(dotPath).isEqualTo(path.toDotPath()); @@ -48,7 +51,7 @@ class PropertyFilterTest { void toDotPathShouldWork(String value) { PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class) - .append(value); + .append(value); assertThat(path.toDotPath()).isEqualTo(value); } @@ -57,7 +60,7 @@ class PropertyFilterTest { void toDotPathWithReplacementShouldWork(String value, String newDotPath) { PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class) - .append(value); + .append(value); String dotPath; dotPath = path.toDotPath(null); assertThat(dotPath).isEqualTo(path.toDotPath()); @@ -70,7 +73,7 @@ class PropertyFilterTest { void getSegmentShouldWork(String value, String segment) { PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class) - .append(value); + .append(value); assertThat(path.getSegment()).isEqualTo(segment); } @@ -79,10 +82,12 @@ class PropertyFilterTest { void leafSegmentShouldWork(String value, String segment) { PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class) - .append(value); + .append(value); PropertyFilter.RelaxedPropertyPath leafProperty = path.getLeafProperty(); assertThat(leafProperty.getType()).isEqualTo(path.getType()); assertThat(leafProperty.getSegment()).isEqualTo(segment); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTests.java similarity index 94% rename from src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTests.java index 8aaff20cc..476e24ed8 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/PropertyTraverserTests.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -24,17 +22,20 @@ import java.util.Set; import java.util.TreeMap; import org.junit.jupiter.api.Test; + import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.neo4j.integration.movies.shared.Movie; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class PropertyTraverserTest { +class PropertyTraverserTests { private final Neo4jMappingContext ctx; - PropertyTraverserTest() { + PropertyTraverserTests() { this.ctx = new Neo4jMappingContext(); Set> entities = new HashSet<>(); entities.add(Movie.class); @@ -47,8 +48,8 @@ class PropertyTraverserTest { PropertyTraverser traverser = new PropertyTraverser(this.ctx); Map includedProperties = new TreeMap<>(); - traverser.traverse(Movie.class, (path, property) -> includedProperties.put(path.toString(), property.isAssociation() && !property.isAnnotationPresent( - TargetNode.class))); + traverser.traverse(Movie.class, (path, property) -> includedProperties.put(path.toString(), + property.isAssociation() && !property.isAnnotationPresent(TargetNode.class))); Map expected = new LinkedHashMap<>(); expected.put("Movie.actors", true); @@ -134,8 +135,7 @@ class PropertyTraverserTest { PropertyTraverser traverser = new PropertyTraverser(this.ctx); Map includedProperties = new TreeMap<>(); - traverser.traverse(Movie.class, - (path, property) -> !property.isAssociation(), + traverser.traverse(Movie.class, (path, property) -> !property.isAssociation(), (path, property) -> includedProperties.put(path.toString(), property.isAssociation())); Map expected = new LinkedHashMap<>(); @@ -152,8 +152,8 @@ class PropertyTraverserTest { PropertyTraverser traverser = new PropertyTraverser(this.ctx); Map includedProperties = new TreeMap<>(); traverser.traverse(Movie.class, - (path, property) -> property.getName().equals("directors") || (path.toDotPath().startsWith("directors.") - && property.getName().equals("name")), + (path, property) -> property.getName().equals("directors") + || (path.toDotPath().startsWith("directors.") && property.getName().equals("name")), (path, property) -> includedProperties.put(path.toString(), property.isAssociation())); Map expected = new LinkedHashMap<>(); @@ -162,4 +162,5 @@ class PropertyTraverserTest { assertThat(includedProperties).containsExactlyEntriesOf(expected); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTests.java similarity index 82% rename from src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTests.java index 773f1e940..6d565cd91 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/AuditingBeforeBindCallbackTests.java @@ -15,6 +15,17 @@ */ package org.springframework.data.neo4j.core.mapping.callback; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; @@ -25,27 +36,17 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.Arrays; -import java.util.HashSet; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.core.Ordered; -import org.springframework.data.auditing.IsNewAwareAuditingHandler; -import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; - /** * @author Michael J. Simons */ -class AuditingBeforeBindCallbackTest { +class AuditingBeforeBindCallbackTests { IsNewAwareAuditingHandler spyOnHandler; AuditingBeforeBindCallback callback; @BeforeEach - public void setUp() { + void setUp() { Neo4jMappingContext mappingContext = new Neo4jMappingContext(); mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Sample.class, ImmutableSample.class))); @@ -53,8 +54,8 @@ class AuditingBeforeBindCallbackTest { IsNewAwareAuditingHandler originalHandler = new IsNewAwareAuditingHandler( new PersistentEntities(Arrays.asList(mappingContext))); - spyOnHandler = spy(originalHandler); - callback = new AuditingBeforeBindCallback(() -> spyOnHandler); + this.spyOnHandler = spy(originalHandler); + this.callback = new AuditingBeforeBindCallback(() -> this.spyOnHandler); } @Test @@ -67,12 +68,12 @@ class AuditingBeforeBindCallbackTest { void triggersCreationMarkForObjectWithEmptyId() { Sample sample = new Sample(); - sample = (Sample) callback.onBeforeBind(sample); + sample = (Sample) this.callback.onBeforeBind(sample); assertThat(sample.created).isNotNull(); assertThat(sample.modified).isNotNull(); - verify(spyOnHandler, times(1)).markCreated(sample); - verify(spyOnHandler, times(0)).markModified(any()); + verify(this.spyOnHandler, times(1)).markCreated(sample); + verify(this.spyOnHandler, times(0)).markModified(any()); } @Test @@ -81,19 +82,19 @@ class AuditingBeforeBindCallbackTest { Sample sample = new Sample(); sample.id = "id"; sample.version = 1L; - sample = (Sample) callback.onBeforeBind(sample); + sample = (Sample) this.callback.onBeforeBind(sample); assertThat(sample.created).isNull(); assertThat(sample.modified).isNotNull(); - verify(spyOnHandler, times(0)).markCreated(any()); - verify(spyOnHandler, times(1)).markModified(sample); + verify(this.spyOnHandler, times(0)).markCreated(any()); + verify(this.spyOnHandler, times(1)).markModified(sample); } @Test void hasExplicitOrder() { - assertThat(callback).isInstanceOf(Ordered.class); - assertThat(callback.getOrder()).isEqualTo(100); + assertThat(this.callback).isInstanceOf(Ordered.class); + assertThat(this.callback.getOrder()).isEqualTo(100); } @Test diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTests.java similarity index 71% rename from src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTests.java index 1de3b33c0..e2d1d70b9 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/IdPopulatorTests.java @@ -15,62 +15,67 @@ */ package org.springframework.data.neo4j.core.mapping.callback; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.IdDescription; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdGenerator; +import org.springframework.data.neo4j.core.schema.Node; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.neo4j.core.mapping.Constants; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.core.schema.GeneratedValue; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.mapping.IdDescription; -import org.springframework.data.neo4j.core.schema.IdGenerator; -import org.springframework.data.neo4j.core.schema.Node; - @ExtendWith(MockitoExtension.class) -class IdPopulatorTest { +class IdPopulatorTests { - @Mock private Neo4jMappingContext neo4jMappingContext; + @Mock + private Neo4jMappingContext neo4jMappingContext; - @Mock private Neo4jPersistentEntity nodeDescription; + @Mock + private Neo4jPersistentEntity nodeDescription; @Test void shouldRejectNullMappingContext() { - Assertions.assertThatIllegalArgumentException().isThrownBy(() -> new IdPopulator(null)) - .withMessage("A mapping context is required"); + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> new IdPopulator(null)) + .withMessage("A mapping context is required"); } @Test void shouldRejectNullEntity() { - IdPopulator idPopulator = new IdPopulator(neo4jMappingContext); - Assertions.assertThatIllegalArgumentException().isThrownBy(() -> idPopulator.populateIfNecessary(null)) - .withMessage("Entity may not be null"); + IdPopulator idPopulator = new IdPopulator(this.neo4jMappingContext); + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> idPopulator.populateIfNecessary(null)) + .withMessage("Entity may not be null"); } @Test void shouldIgnoreInternalIdGenerator() { IdDescription toBeReturned = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, true); - doReturn(toBeReturned).when(nodeDescription).getIdDescription(); - doReturn(nodeDescription).when(neo4jMappingContext).getRequiredPersistentEntity(Sample.class); + doReturn(toBeReturned).when(this.nodeDescription).getIdDescription(); + doReturn(this.nodeDescription).when(this.neo4jMappingContext).getRequiredPersistentEntity(Sample.class); - IdPopulator idPopulator = new IdPopulator(neo4jMappingContext); + IdPopulator idPopulator = new IdPopulator(this.neo4jMappingContext); Sample sample = new Sample(); assertThat(idPopulator.populateIfNecessary(sample)).isSameAs(sample); - verify(nodeDescription).getIdDescription(); - verify(neo4jMappingContext).getRequiredPersistentEntity(Sample.class); + verify(this.nodeDescription).getIdDescription(); + verify(this.neo4jMappingContext).getRequiredPersistentEntity(Sample.class); - verifyNoMoreInteractions(nodeDescription, neo4jMappingContext); + verifyNoMoreInteractions(this.nodeDescription, this.neo4jMappingContext); } @Test @@ -100,14 +105,19 @@ class IdPopulatorTest { void shouldNotFailWithNPEOnMissingIDGenerator() { IdPopulator idPopulator = new IdPopulator(new Neo4jMappingContext()); - assertThatIllegalStateException().isThrownBy(() -> idPopulator.populateIfNecessary(new ImplicitEntityWithoutId())) - .withMessage("Cannot persist implicit entity due to missing id property on " + ImplicitEntityWithoutId.class); + assertThatIllegalStateException() + .isThrownBy(() -> idPopulator.populateIfNecessary(new ImplicitEntityWithoutId())) + .withMessage( + "Cannot persist implicit entity due to missing id property on " + ImplicitEntityWithoutId.class); } @Node static class Sample { - @Id @GeneratedValue(DummyIdGenerator.class) private String theId; + @Id + @GeneratedValue(DummyIdGenerator.class) + private String theId; + } static class ImplicitEntityWithoutId { @@ -120,5 +130,7 @@ class IdPopulatorTest { public String generateId(String primaryLabel, Object entity) { return "Not necessary unique."; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ImmutableSample.java b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ImmutableSample.java index c3c2550e5..46eb65338 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ImmutableSample.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ImmutableSample.java @@ -15,12 +15,13 @@ */ package org.springframework.data.neo4j.core.mapping.callback; +import java.util.Date; +import java.util.Objects; + import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; -import java.util.Date; - /** * @author Michael J. Simons */ @@ -28,8 +29,10 @@ public final class ImmutableSample { @Id private final String id; + @CreatedDate private final Date created; + @LastModifiedDate private final Date modified; @@ -57,6 +60,19 @@ public final class ImmutableSample { return this.modified; } + public ImmutableSample withId(String newId) { + return Objects.equals(this.id, newId) ? this : new ImmutableSample(newId, this.created, this.modified); + } + + public ImmutableSample withCreated(Date newCreated) { + return (this.created != newCreated) ? new ImmutableSample(this.id, newCreated, this.modified) : this; + } + + public ImmutableSample withModified(Date newModified) { + return (this.modified != newModified) ? new ImmutableSample(this.id, this.created, newModified) : this; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -67,47 +83,36 @@ public final class ImmutableSample { final ImmutableSample other = (ImmutableSample) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$created = this.getCreated(); final Object other$created = other.getCreated(); - if (this$created == null ? other$created != null : !this$created.equals(other$created)) { + if (!Objects.equals(this$created, other$created)) { return false; } final Object this$modified = this.getModified(); final Object other$modified = other.getModified(); - if (this$modified == null ? other$modified != null : !this$modified.equals(other$modified)) { - return false; - } - return true; + return Objects.equals(this$modified, other$modified); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $created = this.getCreated(); - result = result * PRIME + ($created == null ? 43 : $created.hashCode()); + result = result * PRIME + (($created != null) ? $created.hashCode() : 43); final Object $modified = this.getModified(); - result = result * PRIME + ($modified == null ? 43 : $modified.hashCode()); + result = result * PRIME + (($modified != null) ? $modified.hashCode() : 43); return result; } + @Override public String toString() { - return "ImmutableSample(id=" + this.getId() + ", created=" + this.getCreated() + ", modified=" + this.getModified() + ")"; + return "ImmutableSample(id=" + this.getId() + ", created=" + this.getCreated() + ", modified=" + + this.getModified() + ")"; } - public ImmutableSample withId(String newId) { - return this.id == newId ? this : new ImmutableSample(newId, this.created, this.modified); - } - - public ImmutableSample withCreated(Date newCreated) { - return this.created == newCreated ? this : new ImmutableSample(this.id, newCreated, this.modified); - } - - public ImmutableSample withModified(Date newModified) { - return this.modified == newModified ? this : new ImmutableSample(this.id, this.created, newModified); - } } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTests.java similarity index 82% rename from src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTest.java rename to src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTests.java index e356734f6..42035ce14 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/ReactiveAuditingBeforeBindCallbackTests.java @@ -15,6 +15,19 @@ */ package org.springframework.data.neo4j.core.mapping.callback; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.Ordered; +import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; @@ -25,30 +38,17 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.util.Arrays; -import java.util.HashSet; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.core.Ordered; -import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; -import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; - /** * @author Michael J. Simons */ -class ReactiveAuditingBeforeBindCallbackTest { +class ReactiveAuditingBeforeBindCallbackTests { ReactiveIsNewAwareAuditingHandler spyOnHandler; ReactiveAuditingBeforeBindCallback callback; @BeforeEach - public void setUp() { + void setUp() { Neo4jMappingContext mappingContext = new Neo4jMappingContext(); mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Sample.class, ImmutableSample.class))); @@ -56,8 +56,8 @@ class ReactiveAuditingBeforeBindCallbackTest { ReactiveIsNewAwareAuditingHandler originalHandler = new ReactiveIsNewAwareAuditingHandler( new PersistentEntities(Arrays.asList(mappingContext))); - spyOnHandler = spy(originalHandler); - callback = new ReactiveAuditingBeforeBindCallback(() -> spyOnHandler); + this.spyOnHandler = spy(originalHandler); + this.callback = new ReactiveAuditingBeforeBindCallback(() -> this.spyOnHandler); } @Test @@ -70,13 +70,13 @@ class ReactiveAuditingBeforeBindCallbackTest { void triggersCreationMarkForObjectWithEmptyId() { Sample sample = new Sample(); - StepVerifier.create(callback.onBeforeBind(sample)).expectNextMatches(s -> { + StepVerifier.create(this.callback.onBeforeBind(sample)).expectNextMatches(s -> { Sample auditedObject = (Sample) s; return auditedObject.created != null && auditedObject.modified != null; }).verifyComplete(); - verify(spyOnHandler, times(1)).markCreated(sample); - verify(spyOnHandler, times(0)).markModified(any()); + verify(this.spyOnHandler, times(1)).markCreated(sample); + verify(this.spyOnHandler, times(0)).markModified(any()); } @Test @@ -86,20 +86,20 @@ class ReactiveAuditingBeforeBindCallbackTest { sample.id = "id"; sample.version = 1L; - StepVerifier.create(callback.onBeforeBind(sample)).expectNextMatches(s -> { + StepVerifier.create(this.callback.onBeforeBind(sample)).expectNextMatches(s -> { Sample auditedObject = (Sample) s; return auditedObject.created == null && auditedObject.modified != null; }).verifyComplete(); - verify(spyOnHandler, times(0)).markCreated(any()); - verify(spyOnHandler, times(1)).markModified(sample); + verify(this.spyOnHandler, times(0)).markCreated(any()); + verify(this.spyOnHandler, times(1)).markModified(sample); } @Test void hasExplicitOrder() { - assertThat(callback).isInstanceOf(Ordered.class); - assertThat(callback.getOrder()).isEqualTo(100); + assertThat(this.callback).isInstanceOf(Ordered.class); + assertThat(this.callback.getOrder()).isEqualTo(100); } @Test diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/Sample.java b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/Sample.java index 4dadb5be6..2de5be171 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/callback/Sample.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/callback/Sample.java @@ -27,8 +27,16 @@ import org.springframework.data.annotation.Version; */ class Sample { - @Id String id; - @Version Long version; - @CreatedDate Date created; - @LastModifiedDate Date modified; + @Id + String id; + + @Version + Long version; + + @CreatedDate + Date created; + + @LastModifiedDate + Date modified; + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/AbstractR.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/AbstractR.java index 25939be1a..65ba77e5e 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/AbstractR.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/AbstractR.java @@ -20,28 +20,28 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * @author Michael J. Simons * @param The type of the target entity + * @author Michael J. Simons */ @RelationshipProperties public abstract class AbstractR { - @RelationshipId - private Long id; - @TargetNode T target; - public AbstractR(T target) { - this.target = target; - } + @RelationshipId + private Long id; private String p1; private String p2; + public AbstractR(T target) { + this.target = target; + } + public String getP1() { - return p1; + return this.p1; } public void setP1(String p1) { @@ -49,18 +49,16 @@ public abstract class AbstractR { } public String getP2() { - return p2; + return this.p2; } public void setP2(String p2) { this.p2 = p2; } - @Override public String toString() { - return "AbstractR{" + - "target=" + target + - ", p1='" + p1 + '\'' + - ", p2='" + p2 + '\'' + - '}'; + @Override + public String toString() { + return "AbstractR{" + "target=" + this.target + ", p1='" + this.p1 + '\'' + ", p2='" + this.p2 + '\'' + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/B.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/B.java index c232a1d0a..bce6001a0 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/B.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/B.java @@ -25,7 +25,8 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class B { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; private String name; @@ -35,21 +36,20 @@ public class B { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } - @Override public String toString() { - return "B{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; + @Override + public String toString() { + return "B{" + "id=" + this.id + ", name='" + this.name + '\'' + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/C.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/C.java index bdf0197aa..8702b3df0 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/C.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/C.java @@ -25,7 +25,8 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class C { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; private String name; @@ -35,21 +36,20 @@ public class C { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } - @Override public String toString() { - return "C{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; + @Override + public String toString() { + return "C{" + "id=" + this.id + ", name='" + this.name + '\'' + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/P.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/P.java index 5a0eb9773..ff79a9f2d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/P.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/P.java @@ -26,27 +26,28 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class P { - @Id @GeneratedValue + @Relationship("R") + R1 b; + + @Relationship("R") + R2 c; + + @Id + @GeneratedValue private Long id; private String name; - @Relationship(value = "R") - R1 b; - - @Relationship(value = "R") - R2 c; - public P(String name) { this.name = name; } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -54,7 +55,7 @@ public class P { } public R1 getB() { - return b; + return this.b; } public void setB(R1 b) { @@ -62,19 +63,16 @@ public class P { } public R2 getC() { - return c; + return this.c; } public void setC(R2 c) { this.c = c; } - @Override public String toString() { - return "A{" + - "id=" + id + - ", name='" + name + '\'' + - ", b=" + b + - ", c=" + c + - '}'; + @Override + public String toString() { + return "A{" + "id=" + this.id + ", name='" + this.name + '\'' + ", b=" + this.b + ", c=" + this.c + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R1.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R1.java index 80b25c372..98c45416d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R1.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R1.java @@ -23,4 +23,5 @@ public class R1 extends AbstractR { public R1(B target) { super(target); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R2.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R2.java index 3e1e623d6..e74763a14 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R2.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1446/R2.java @@ -23,4 +23,5 @@ public class R2 extends AbstractR { public R2(C target) { super(target); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/A_S3.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/A_S3.java index 3815e4a00..0a781d09e 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/A_S3.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/A_S3.java @@ -28,7 +28,8 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class A_S3 { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; private String name; @@ -38,4 +39,5 @@ public class A_S3 { public A_S3(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/B_S3.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/B_S3.java index 988b18c32..c4fe274a9 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/B_S3.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/B_S3.java @@ -28,4 +28,5 @@ public class B_S3 extends RelatedThing { public B_S3(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/C_S3.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/C_S3.java index 54c638fc0..649c18a4b 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/C_S3.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/C_S3.java @@ -28,4 +28,5 @@ public class C_S3 extends RelatedThing { public C_S3(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/R_S3.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/R_S3.java index ec2091690..6147caaf7 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/R_S3.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/R_S3.java @@ -20,23 +20,24 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; /** + * @param the type of the related thing * @author Michael J. Simons - * @param The type of the related thing */ @RelationshipProperties public class R_S3 { + @TargetNode + T target; + @RelationshipId private Long id; - @TargetNode - T target; + private String p1; + + private String p2; public R_S3(T target) { this.target = target; } - private String p1; - - private String p2; } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/RelatedThing.java b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/RelatedThing.java index 5d64d08a3..0047e4166 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/RelatedThing.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/datagraph1448/RelatedThing.java @@ -25,6 +25,8 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public abstract class RelatedThing { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Properties.java b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Properties.java index 1c927a7c0..5995c7bfa 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Properties.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Properties.java @@ -21,8 +21,8 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; /** + * @param the crux of this class * @author Michael J. Simons - * @param The crux of this class */ @RelationshipProperties public class Properties { @@ -33,4 +33,5 @@ public class Properties { @TargetNode T target; + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Source.java b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Source.java index f5e62a675..149e092eb 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Source.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Source.java @@ -24,4 +24,5 @@ public class Source { @Relationship("IS_RELATED_TO") private Properties target; + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Target.java b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Target.java index 0b679acfa..e2d128554 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Target.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/genericRelProperties/Target.java @@ -19,4 +19,5 @@ package org.springframework.data.neo4j.core.mapping.genericRelProperties; * @author Michael J. Simons */ public class Target { + } diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/gh2574/Model.java b/src/test/java/org/springframework/data/neo4j/core/mapping/gh2574/Model.java index 8f2a79988..3d1bcadea 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/gh2574/Model.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/gh2574/Model.java @@ -25,13 +25,18 @@ import org.springframework.data.neo4j.core.schema.Node; */ public abstract class Model { + private Model() { + } + /** * Shut up checkstyle. */ @Node public abstract static class A1 { + @Id String id; + } /** @@ -39,6 +44,7 @@ public abstract class Model { */ @Node public abstract static class A2 extends A1 { + } /** @@ -46,6 +52,7 @@ public abstract class Model { */ @Node public abstract static class A3 extends A2 { + } /** @@ -53,6 +60,7 @@ public abstract class Model { */ @Node public static class A4 extends A3 { + } /** @@ -60,8 +68,10 @@ public abstract class Model { */ @Node public abstract static class B1 { + @Id String id; + } /** @@ -69,6 +79,7 @@ public abstract class Model { */ @Node public abstract static class B2 extends B1 { + } /** @@ -76,6 +87,7 @@ public abstract class Model { */ @Node public static class B2a extends B2 { + } /** @@ -83,6 +95,7 @@ public abstract class Model { */ @Node public abstract static class B3 extends B2 { + } /** @@ -90,8 +103,7 @@ public abstract class Model { */ @Node public static class B3a extends B3 { + } - private Model() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTest.java b/src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTests.java similarity index 97% rename from src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTest.java rename to src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTests.java index fcb6abcf0..734f5a10d 100644 --- a/src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/support/RetryExceptionPredicateTests.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.support; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -24,14 +22,16 @@ import org.junit.platform.commons.util.ReflectionUtils; import org.neo4j.driver.exceptions.DiscoveryException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; + import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.data.neo4j.core.Neo4jPersistenceExceptionTranslator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Opium fürs Volk */ -class RetryExceptionPredicateTest { +class RetryExceptionPredicateTests { @ParameterizedTest @ValueSource(strings = { "Transaction must be open, but has already been closed", @@ -90,4 +90,5 @@ class RetryExceptionPredicateTest { RetryExceptionPredicate predicate = new RetryExceptionPredicate(); assertThat(predicate.test(ex)).isTrue(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/support/UserAgentTest.java b/src/test/java/org/springframework/data/neo4j/core/support/UserAgentTests.java similarity index 89% rename from src/test/java/org/springframework/data/neo4j/core/support/UserAgentTest.java rename to src/test/java/org/springframework/data/neo4j/core/support/UserAgentTests.java index d517a00bb..05fc2b543 100644 --- a/src/test/java/org/springframework/data/neo4j/core/support/UserAgentTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/support/UserAgentTests.java @@ -15,19 +15,20 @@ */ package org.springframework.data.neo4j.core.support; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class UserAgentTest { +class UserAgentTests { @Test void toStringShouldWork() { assertThat(UserAgent.INSTANCE.toString()) - .matches("Java/.+ \\(.+\\) neo4j-java/.+ spring-data/.+ spring-data-neo4j/.+"); + .matches("Java/.+ \\(.+\\) neo4j-java/.+ spring-data/.+ spring-data-neo4j/.+"); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/AssertableBookmarkManager.java b/src/test/java/org/springframework/data/neo4j/core/transaction/AssertableBookmarkManager.java index 7168132e1..d9b745ab1 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/AssertableBookmarkManager.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/AssertableBookmarkManager.java @@ -29,17 +29,19 @@ import org.neo4j.driver.Bookmark; */ final class AssertableBookmarkManager extends AbstractBookmarkManager { - boolean getBookmarksCalled = false; final Map, Boolean> updateBookmarksCalled = new HashMap<>(); + boolean getBookmarksCalled = false; + @Override public Collection getBookmarks() { - getBookmarksCalled = true; + this.getBookmarksCalled = true; return Collections.emptyList(); } @Override public void updateBookmarks(Collection usedBookmarks, Collection newBookmarks) { - updateBookmarksCalled.put(newBookmarks, true); + this.updateBookmarksCalled.put(newBookmarks, true); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkForTesting.java b/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkForTesting.java index 718039df2..8c9b54b8b 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkForTesting.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkForTesting.java @@ -32,12 +32,12 @@ record BookmarkForTesting(String value) implements Bookmark { @Override @SuppressWarnings({ "deprecation", "RedundantSuppression" }) public Set values() { - return Set.of(value); + return Set.of(this.value); } @Override @SuppressWarnings({ "deprecation", "RedundantSuppression" }) public boolean isEmpty() { - return value.isBlank(); + return this.value.isBlank(); } } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTest.java b/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTests.java similarity index 86% rename from src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTest.java rename to src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTests.java index 727b5b309..8b5e6a92c 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/BookmarkManagerTests.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.core.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -28,14 +25,21 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.neo4j.driver.Bookmark; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Dmitriy Tverdiakov * @author Michael J. Simons */ -class BookmarkManagerTest { +class BookmarkManagerTests { + + static Neo4jBookmarkManager newBookmarkManager(Class type) throws Exception { + return type.getDeclaredConstructor(Supplier.class).newInstance((Supplier) null); + } @ParameterizedTest - @ValueSource(classes = {DefaultBookmarkManager.class, ReactiveDefaultBookmarkManager.class}) + @ValueSource(classes = { DefaultBookmarkManager.class, ReactiveDefaultBookmarkManager.class }) void shouldReturnBookmarksCopy(Class bookmarkManagerType) throws Exception { var manager = newBookmarkManager(bookmarkManagerType); @@ -50,7 +54,7 @@ class BookmarkManagerTest { } @ParameterizedTest - @ValueSource(classes = {DefaultBookmarkManager.class, ReactiveDefaultBookmarkManager.class}) + @ValueSource(classes = { DefaultBookmarkManager.class, ReactiveDefaultBookmarkManager.class }) void shouldReturnUnmodifiableBookmarks(Class bookmarkManagerType) throws Exception { var manager = newBookmarkManager(bookmarkManagerType); @@ -59,14 +63,10 @@ class BookmarkManagerTest { var bookmarks = manager.getBookmarks(); assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> bookmarks.add(Bookmark.from("bookmark 2"))); + .isThrownBy(() -> bookmarks.add(Bookmark.from("bookmark 2"))); assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> bookmarks.remove(Bookmark.from("bookmark 1"))); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(bookmarks::clear); + .isThrownBy(() -> bookmarks.remove(Bookmark.from("bookmark 1"))); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(bookmarks::clear); } - static Neo4jBookmarkManager newBookmarkManager(Class type) throws Exception { - return type.getDeclaredConstructor(Supplier.class).newInstance((Supplier) null); - } } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTest.java b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTests.java similarity index 92% rename from src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTest.java rename to src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTests.java index 0ccedcebe..ba0371655 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jBookmarkManagerTests.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.core.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -28,11 +25,14 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.neo4j.driver.Bookmark; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * @author Gerrit Meier * @author Michael J. Simons */ -class Neo4jBookmarkManagerTest { +class Neo4jBookmarkManagerTests { @Test // GH-2245 void publishesNewBookmarks() { @@ -127,12 +127,19 @@ class Neo4jBookmarkManagerTest { void shouldAlwaysReturnEmptyList() { Neo4jBookmarkManager bookmarkManager = Neo4jBookmarkManager.noop(); - assertThat(bookmarkManager.getBookmarks()) - .isSameAs(Collections.emptyList()) // Might not be that sane to check that but alas - .isEmpty(); + assertThat(bookmarkManager.getBookmarks()).isSameAs(Collections.emptyList()) // Might + // not + // be + // that + // sane + // to + // check + // that + // but + // alas + .isEmpty(); } - @Test void shouldNeverAcceptBookmarks() { @@ -148,5 +155,7 @@ class Neo4jBookmarkManagerTest { bookmarkManager.updateBookmarks(new HashSet<>(), List.of(bookmark)); assertThat(asserted).isFalse(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTests.java similarity index 52% rename from src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java rename to src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTests.java index c339e210e..5aea38ea7 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTests.java @@ -15,29 +15,18 @@ */ package org.springframework.data.neo4j.core.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyMap; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - import java.lang.reflect.Field; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import jakarta.transaction.Status; import jakarta.transaction.UserTransaction; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.Bookmark; @@ -49,9 +38,10 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.DatabaseSelection; -import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.support.BookmarkManagerReference; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; @@ -62,82 +52,106 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + /** * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class Neo4jTransactionManagerTest { +class Neo4jTransactionManagerTests { private final DatabaseSelection databaseSelection = DatabaseSelection.byName("aDatabase"); + private final UserSelection userSelection = UserSelection.connectedUser(); - @Mock private Driver driver; - @Mock private Session session; - @Mock private TypeSystem typeSystem; - @Mock private Transaction transaction; - @Mock private Result statementResult; - @Mock private UserTransaction userTransaction; - @Mock private ResultSummary resultSummary; + @Mock + private Driver driver; + + @Mock + private Session session; + + @Mock + private TypeSystem typeSystem; + + @Mock + private Transaction transaction; + + @Mock + private Result statementResult; + + @Mock + private UserTransaction userTransaction; + + @Mock + private ResultSummary resultSummary; @Test void shouldWorkWithoutSynchronizations() { - Transaction optionalTransaction = Neo4jTransactionManager.retrieveTransaction(driver, databaseSelection, - userSelection); + Transaction optionalTransaction = Neo4jTransactionManager.retrieveTransaction(this.driver, + this.databaseSelection, this.userSelection); assertThat(optionalTransaction).isNull(); - verifyNoInteractions(driver, session, transaction); + verifyNoInteractions(this.driver, this.session, this.transaction); } @Test void triggerCommitCorrectly() { - when(driver.session(any(SessionConfig.class))).thenReturn(session); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); - when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); - when(session.isOpen()).thenReturn(true); - when(statementResult.consume()).thenReturn(resultSummary); - when(transaction.isOpen()).thenReturn(true, false); + given(this.driver.session(any(SessionConfig.class))).willReturn(this.session); + given(this.session.beginTransaction(any(TransactionConfig.class))).willReturn(this.transaction); + given(this.transaction.run(anyString(), anyMap())).willReturn(this.statementResult); + given(this.session.isOpen()).willReturn(true); + given(this.statementResult.consume()).willReturn(this.resultSummary); + given(this.transaction.isOpen()).willReturn(true, false); - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + Neo4jTransactionManager txManager = new Neo4jTransactionManager(this.driver); TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); client.query("RETURN 1").run(); txManager.commit(txStatus); - verify(driver).session(any(SessionConfig.class)); + verify(this.driver).session(any(SessionConfig.class)); - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); + verify(this.session).isOpen(); + verify(this.session).beginTransaction(any(TransactionConfig.class)); - verify(transaction, times(2)).isOpen(); - verify(transaction).commit(); - verify(transaction).close(); + verify(this.transaction, times(2)).isOpen(); + verify(this.transaction).commit(); + verify(this.transaction).close(); - verify(session).close(); + verify(this.session).close(); } @Test void usesBookmarksCorrectly() throws Exception { - when(driver.session(any(SessionConfig.class))).thenReturn(session); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + given(this.driver.session(any(SessionConfig.class))).willReturn(this.session); + given(this.session.beginTransaction(any(TransactionConfig.class))).willReturn(this.transaction); Set bookmark = Set.of(new BookmarkForTesting("blubb")); - when(session.lastBookmarks()).thenReturn(bookmark); - when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); - when(session.isOpen()).thenReturn(true); - when(transaction.isOpen()).thenReturn(true, false); - when(statementResult.consume()).thenReturn(resultSummary); + given(this.session.lastBookmarks()).willReturn(bookmark); + given(this.transaction.run(anyString(), anyMap())).willReturn(this.statementResult); + given(this.session.isOpen()).willReturn(true); + given(this.transaction.isOpen()).willReturn(true, false); + given(this.statementResult.consume()).willReturn(this.resultSummary); - Neo4jTransactionManager txManager = spy(new Neo4jTransactionManager(driver)); + Neo4jTransactionManager txManager = spy(new Neo4jTransactionManager(this.driver)); AssertableBookmarkManager bookmarkManager = new AssertableBookmarkManager(); injectBookmarkManager(txManager, bookmarkManager); TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); - Neo4jClient client = Neo4jClient.create(driver); + Neo4jClient client = Neo4jClient.create(this.driver); client.query("RETURN 1").run(); txManager.commit(txStatus); @@ -145,8 +159,7 @@ class Neo4jTransactionManagerTest { verify(txManager).doBegin(any(), any(TransactionDefinition.class)); assertThat(bookmarkManager.getBookmarksCalled).isTrue(); verify(txManager).doCommit(any(DefaultTransactionStatus.class)); - assertThat(bookmarkManager.updateBookmarksCalled) - .containsEntry(bookmark, true); + assertThat(bookmarkManager.updateBookmarksCalled).containsEntry(bookmark, true); } private void injectBookmarkManager(Neo4jTransactionManager txManager, Neo4jBookmarkManager value) @@ -165,20 +178,24 @@ class Neo4jTransactionManagerTest { AtomicBoolean sessionIsOpen = new AtomicBoolean(true); AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - when(driver.session(any(SessionConfig.class))).thenReturn(session); + given(Neo4jTransactionManagerTests.this.driver.session(any(SessionConfig.class))) + .willReturn(Neo4jTransactionManagerTests.this.session); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); - doAnswer(invocation -> { + given(Neo4jTransactionManagerTests.this.session.beginTransaction(any(TransactionConfig.class))) + .willReturn(Neo4jTransactionManagerTests.this.transaction); + + BDDMockito.doAnswer(invocation -> { sessionIsOpen.set(false); return null; - }).when(session).close(); - when(session.isOpen()).thenAnswer(invocation -> sessionIsOpen.get()); + }).when(Neo4jTransactionManagerTests.this.session).close(); + given(Neo4jTransactionManagerTests.this.session.isOpen()).willAnswer(invocation -> sessionIsOpen.get()); - doAnswer(invocation -> { + BDDMockito.doAnswer(invocation -> { transactionIsOpen.set(false); return null; - }).when(transaction).close(); - when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); + }).when(Neo4jTransactionManagerTests.this.transaction).close(); + given(Neo4jTransactionManagerTests.this.transaction.isOpen()) + .willAnswer(invocation -> transactionIsOpen.get()); } @AfterEach @@ -198,9 +215,10 @@ class Neo4jTransactionManagerTest { @Test void shouldUseTxFromNeo4jTxManager() { - Neo4jTransactionManager txManager = Neo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(() -> databaseSelection) - .build(); + Neo4jTransactionManager txManager = Neo4jTransactionManager + .with(Neo4jTransactionManagerTests.this.driver) + .withDatabaseSelectionProvider(() -> Neo4jTransactionManagerTests.this.databaseSelection) + .build(); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -210,34 +228,38 @@ class Neo4jTransactionManagerTest { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + assertThat( + TransactionSynchronizationManager.hasResource(Neo4jTransactionManagerTests.this.driver)) + .isTrue(); - Transaction optionalTransaction = Neo4jTransactionManager.retrieveTransaction(driver, - databaseSelection, - userSelection); + Transaction optionalTransaction = Neo4jTransactionManager.retrieveTransaction( + Neo4jTransactionManagerTests.this.driver, + Neo4jTransactionManagerTests.this.databaseSelection, + Neo4jTransactionManagerTests.this.userSelection); assertThat(optionalTransaction).isNotNull(); transactionStatus.setRollbackOnly(); } }); - verify(driver).session(any(SessionConfig.class)); + verify(Neo4jTransactionManagerTests.this.driver).session(any(SessionConfig.class)); - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); + verify(Neo4jTransactionManagerTests.this.session).isOpen(); + verify(Neo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(Neo4jTransactionManagerTests.this.session).close(); - verify(transaction, times(2)).isOpen(); - verify(transaction).rollback(); - verify(transaction).close(); + verify(Neo4jTransactionManagerTests.this.transaction, times(2)).isOpen(); + verify(Neo4jTransactionManagerTests.this.transaction).rollback(); + verify(Neo4jTransactionManagerTests.this.transaction).close(); } @Test void shouldParticipateInOngoingTransaction() { - Neo4jTransactionManager txManager = Neo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(() -> databaseSelection) - .build(); + Neo4jTransactionManager txManager = Neo4jTransactionManager + .with(Neo4jTransactionManagerTests.this.driver) + .withDatabaseSelectionProvider(() -> Neo4jTransactionManagerTests.this.databaseSelection) + .build(); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -245,9 +267,10 @@ class Neo4jTransactionManagerTest { @Override protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - Transaction outerNativeTransaction = Neo4jTransactionManager.retrieveTransaction(driver, - databaseSelection, - userSelection); + Transaction outerNativeTransaction = Neo4jTransactionManager.retrieveTransaction( + Neo4jTransactionManagerTests.this.driver, + Neo4jTransactionManagerTests.this.databaseSelection, + Neo4jTransactionManagerTests.this.userSelection); assertThat(outerNativeTransaction).isNotNull(); assertThat(outerStatus.isNewTransaction()).isTrue(); @@ -258,9 +281,10 @@ class Neo4jTransactionManagerTest { assertThat(innerStatus.isNewTransaction()).isFalse(); - Transaction innerNativeTransaction = Neo4jTransactionManager.retrieveTransaction(driver, - databaseSelection, - userSelection); + Transaction innerNativeTransaction = Neo4jTransactionManager.retrieveTransaction( + Neo4jTransactionManagerTests.this.driver, + Neo4jTransactionManagerTests.this.databaseSelection, + Neo4jTransactionManagerTests.this.userSelection); assertThat(innerNativeTransaction).isNotNull(); } }); @@ -269,15 +293,15 @@ class Neo4jTransactionManagerTest { } }); - verify(driver).session(any(SessionConfig.class)); + verify(Neo4jTransactionManagerTests.this.driver).session(any(SessionConfig.class)); - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); + verify(Neo4jTransactionManagerTests.this.session).isOpen(); + verify(Neo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(Neo4jTransactionManagerTests.this.session).close(); - verify(transaction, times(2)).isOpen(); - verify(transaction).rollback(); - verify(transaction).close(); + verify(Neo4jTransactionManagerTests.this.transaction, times(2)).isOpen(); + verify(Neo4jTransactionManagerTests.this.transaction).rollback(); + verify(Neo4jTransactionManagerTests.this.transaction).close(); } } @@ -288,10 +312,11 @@ class Neo4jTransactionManagerTest { @Test void shouldParticipateInOngoingTransactionWithCommit() throws Exception { - when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, - Status.STATUS_ACTIVE); + given(Neo4jTransactionManagerTests.this.userTransaction.getStatus()) + .willReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + JtaTransactionManager txManager = new JtaTransactionManager( + Neo4jTransactionManagerTests.this.userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -301,37 +326,43 @@ class Neo4jTransactionManagerTest { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + assertThat( + TransactionSynchronizationManager.hasResource(Neo4jTransactionManagerTests.this.driver)) + .isFalse(); - Transaction nativeTransaction = Neo4jTransactionManager.retrieveTransaction(driver, - databaseSelection, - userSelection); + Transaction nativeTransaction = Neo4jTransactionManager.retrieveTransaction( + Neo4jTransactionManagerTests.this.driver, + Neo4jTransactionManagerTests.this.databaseSelection, + Neo4jTransactionManagerTests.this.userSelection); assertThat(nativeTransaction).isNotNull(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + assertThat( + TransactionSynchronizationManager.hasResource(Neo4jTransactionManagerTests.this.driver)) + .isTrue(); } }); - verify(userTransaction).begin(); + verify(Neo4jTransactionManagerTests.this.userTransaction).begin(); - verify(driver).session(any(SessionConfig.class)); + verify(Neo4jTransactionManagerTests.this.driver).session(any(SessionConfig.class)); - verify(session, times(2)).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); + verify(Neo4jTransactionManagerTests.this.session, times(2)).isOpen(); + verify(Neo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(Neo4jTransactionManagerTests.this.session).close(); - verify(transaction, times(3)).isOpen(); - verify(transaction).commit(); - verify(transaction).close(); + verify(Neo4jTransactionManagerTests.this.transaction, times(3)).isOpen(); + verify(Neo4jTransactionManagerTests.this.transaction).commit(); + verify(Neo4jTransactionManagerTests.this.transaction).close(); } @Test void shouldParticipateInOngoingTransactionWithRollback() throws Exception { - when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, - Status.STATUS_ACTIVE); + given(Neo4jTransactionManagerTests.this.userTransaction.getStatus()) + .willReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + JtaTransactionManager txManager = new JtaTransactionManager( + Neo4jTransactionManagerTests.this.userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -341,32 +372,40 @@ class Neo4jTransactionManagerTest { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + assertThat( + TransactionSynchronizationManager.hasResource(Neo4jTransactionManagerTests.this.driver)) + .isFalse(); - Transaction nativeTransaction = Neo4jTransactionManager.retrieveTransaction(driver, - databaseSelection, - userSelection); + Transaction nativeTransaction = Neo4jTransactionManager.retrieveTransaction( + Neo4jTransactionManagerTests.this.driver, + Neo4jTransactionManagerTests.this.databaseSelection, + Neo4jTransactionManagerTests.this.userSelection); assertThat(nativeTransaction).isNotNull(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + assertThat( + TransactionSynchronizationManager.hasResource(Neo4jTransactionManagerTests.this.driver)) + .isTrue(); transactionStatus.setRollbackOnly(); } }); - verify(userTransaction).begin(); - verify(userTransaction).rollback(); + verify(Neo4jTransactionManagerTests.this.userTransaction).begin(); + verify(Neo4jTransactionManagerTests.this.userTransaction).rollback(); - verify(driver).session(any(SessionConfig.class)); + verify(Neo4jTransactionManagerTests.this.driver).session(any(SessionConfig.class)); - verify(session, times(2)).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); + verify(Neo4jTransactionManagerTests.this.session, times(2)).isOpen(); + verify(Neo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(Neo4jTransactionManagerTests.this.session).close(); - verify(transaction, times(3)).isOpen(); - verify(transaction).rollback(); - verify(transaction).close(); + verify(Neo4jTransactionManagerTests.this.transaction, times(3)).isOpen(); + verify(Neo4jTransactionManagerTests.this.transaction).rollback(); + verify(Neo4jTransactionManagerTests.this.transaction).close(); } + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTests.java similarity index 70% rename from src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java rename to src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTests.java index d6db63485..99a348934 100644 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTests.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.core.transaction; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.Duration; import org.junit.jupiter.api.Test; @@ -24,48 +22,43 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.neo4j.driver.TransactionConfig; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.util.StringUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class Neo4jTransactionUtilsTest { +class Neo4jTransactionUtilsTests { - @CsvSource(nullValues = "n/a", - delimiter = '|', - value = { - "n/a| dbA| n/a | userA | There is already an ongoing Spring transaction for the default user of the default database, but you requested 'userA' of 'dbA'", - "dbA| n/a| userA | n/a | There is already an ongoing Spring transaction for 'userA' of 'dbA', but you requested the default user of the default database", - "dbA| dbB| userA | userB | There is already an ongoing Spring transaction for 'userA' of 'dbA', but you requested 'userB' of 'dbB'" - } - ) + @CsvSource(nullValues = "n/a", delimiter = '|', value = { + "n/a| dbA| n/a | userA | There is already an ongoing Spring transaction for the default user of the default database, but you requested 'userA' of 'dbA'", + "dbA| n/a| userA | n/a | There is already an ongoing Spring transaction for 'userA' of 'dbA', but you requested the default user of the default database", + "dbA| dbB| userA | userB | There is already an ongoing Spring transaction for 'userA' of 'dbA', but you requested 'userB' of 'dbB'" }) @ParameterizedTest - void formatOngoingTxInAnotherDbErrorMessageShouldWork(String cdb, String rdb, String cu, String ru, String expected) { + void formatOngoingTxInAnotherDbErrorMessageShouldWork(String cdb, String rdb, String cu, String ru, + String expected) { - DatabaseSelection currentDatabaseSelection = StringUtils.hasText(cdb) ? - DatabaseSelection.byName(cdb) : - DatabaseSelection.undecided(); - DatabaseSelection requestedDatabaseSelection = StringUtils.hasText(rdb) ? - DatabaseSelection.byName(rdb) : - DatabaseSelection.undecided(); - UserSelection currentUserSelection = StringUtils.hasText(cu) ? - UserSelection.impersonate(cu) : - UserSelection.connectedUser(); - UserSelection requestedUserSelection = StringUtils.hasText(ru) ? - UserSelection.impersonate(ru) : - UserSelection.connectedUser(); + DatabaseSelection currentDatabaseSelection = StringUtils.hasText(cdb) ? DatabaseSelection.byName(cdb) + : DatabaseSelection.undecided(); + DatabaseSelection requestedDatabaseSelection = StringUtils.hasText(rdb) ? DatabaseSelection.byName(rdb) + : DatabaseSelection.undecided(); + UserSelection currentUserSelection = StringUtils.hasText(cu) ? UserSelection.impersonate(cu) + : UserSelection.connectedUser(); + UserSelection requestedUserSelection = StringUtils.hasText(ru) ? UserSelection.impersonate(ru) + : UserSelection.connectedUser(); - String result = Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage( - currentDatabaseSelection, requestedDatabaseSelection, currentUserSelection, requestedUserSelection - ); + String result = Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage(currentDatabaseSelection, + requestedDatabaseSelection, currentUserSelection, requestedUserSelection); assertThat(result).isEqualTo(expected); } @ParameterizedTest // GH-2463 - @ValueSource(ints = {Integer.MIN_VALUE, -1, 0, DefaultTransactionDefinition.TIMEOUT_DEFAULT}) + @ValueSource(ints = { Integer.MIN_VALUE, -1, 0, DefaultTransactionDefinition.TIMEOUT_DEFAULT }) void shouldNotApplyNegativeOrZeroTimeOuts(int value) { DefaultTransactionDefinition springDef = new DefaultTransactionDefinition(); @@ -75,7 +68,7 @@ class Neo4jTransactionUtilsTest { } @ParameterizedTest // GH-2463 - @ValueSource(ints = {Integer.MIN_VALUE, -1, 0}) + @ValueSource(ints = { Integer.MIN_VALUE, -1, 0 }) void shouldPreferTxDef(int value) { DefaultTransactionDefinition springDef = new DefaultTransactionDefinition(); @@ -92,4 +85,5 @@ class Neo4jTransactionUtilsTest { TransactionConfig driverConfig = Neo4jTransactionUtils.createTransactionConfigFrom(springDef, 3); assertThat(driverConfig.timeout()).isEqualTo(Duration.ofSeconds(3)); } + } diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java b/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java deleted file mode 100644 index e0a6c0f70..000000000 --- a/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2011-2025 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.core.transaction; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.r2dbc.h2.H2ConnectionConfiguration; -import io.r2dbc.h2.H2ConnectionFactory; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.lang.reflect.Field; -import java.util.Set; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.neo4j.driver.Bookmark; -import org.neo4j.driver.Driver; -import org.neo4j.driver.SessionConfig; -import org.neo4j.driver.TransactionConfig; -import org.neo4j.driver.reactivestreams.ReactiveSession; -import org.neo4j.driver.reactivestreams.ReactiveTransaction; -import org.springframework.data.neo4j.core.DatabaseSelection; -import org.springframework.data.neo4j.core.UserSelection; -import org.springframework.data.neo4j.core.support.BookmarkManagerReference; -import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; -import org.springframework.transaction.reactive.TransactionSynchronizationManager; -import org.springframework.transaction.reactive.TransactionalOperator; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class ReactiveNeo4jTransactionManagerTest { - - private DatabaseSelection databaseSelection = DatabaseSelection.byName("aDatabase"); - private UserSelection userSelection = UserSelection.connectedUser(); - - @Mock private Driver driver; - - @Mock private ReactiveSession session; - @Mock private ReactiveTransaction transaction; - - @BeforeEach - void setUp() { - - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).thenReturn(session); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(transaction)); - when(transaction.rollback()).thenReturn(Mono.empty()); - when(transaction.commit()).thenReturn(Mono.empty()); - when(session.close()).thenReturn(Mono.empty()); - } - - @Test - void shouldWorkWithoutSynchronizations() { - - Mono transactionMono = ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, - databaseSelection, userSelection); - - StepVerifier.create(transactionMono).verifyComplete(); - } - - @Nested - class BasedOnNeo4jTransactions { - @Test - void shouldUseTxFromNeo4jTxManager() { - - ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(() -> Mono.just(databaseSelection)) - .build(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); - - transactionalOperator - .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction().doOnNext(tsm -> { - assertThat(tsm.hasResource(driver)).isTrue(); - transactionStatus.setRollbackOnly(); - }).then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection))) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - verify(transaction).rollback(); - verify(transaction, never()).commit(); - } - - @Test - void shouldParticipateInOngoingTransaction() { - - ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(() -> Mono.just(databaseSelection)) - .build(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); - - transactionalOperator.execute(outerStatus -> { - assertThat(outerStatus.isNewTransaction()).isTrue(); - outerStatus.setRollbackOnly(); - return transactionalOperator.execute(innerStatus -> { - assertThat(innerStatus.isNewTransaction()).isFalse(); - return ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection); - }).then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection)); - }).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - verify(transaction).rollback(); - verify(transaction, never()).commit(); - } - - @Test - void usesBookmarksCorrectly() throws Exception { - - ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(() -> Mono.just(databaseSelection)) - .build(); - - AssertableBookmarkManager bookmarkManager = new AssertableBookmarkManager(); - injectBookmarkManager(txManager, bookmarkManager); - - Set bookmark = Set.of(new BookmarkForTesting("blubb")); - when(session.lastBookmarks()).thenReturn(bookmark); - - TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); - - transactionalOperator - .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue()) - .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection))) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - verify(session).beginTransaction(any(TransactionConfig.class)); - assertThat(bookmarkManager.getBookmarksCalled).isTrue(); - verify(session).close(); - verify(transaction).commit(); - assertThat(bookmarkManager.updateBookmarksCalled) - .containsEntry(bookmark, true); - } - - private void injectBookmarkManager(ReactiveNeo4jTransactionManager txManager, Neo4jBookmarkManager value) - throws NoSuchFieldException, IllegalAccessException { - Field bookmarkManager = ReactiveNeo4jTransactionManager.class.getDeclaredField("bookmarkManager"); - bookmarkManager.setAccessible(true); - bookmarkManager.set(txManager, new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, value)); - } - } - - @Nested - class BasedOnOtherTransactions { - - @Test - void shouldSynchronizeWithExternalWithCommit() { - - R2dbcTransactionManager t = new R2dbcTransactionManager( - new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("test").build())); - - TransactionalOperator transactionalOperator = TransactionalOperator.create(t); - - transactionalOperator - .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isFalse()) - .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection)) - .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue()))) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - verify(transaction).commit(); - verify(transaction, never()).rollback(); - } - - @Test - void shouldSynchronizeWithExternalWithRollback() { - - R2dbcTransactionManager t = new R2dbcTransactionManager( - new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("test").build())); - - TransactionalOperator transactionalOperator = TransactionalOperator.create(t); - - transactionalOperator - .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction().doOnNext(tsm -> { - assertThat(tsm.hasResource(driver)).isFalse(); - transactionStatus.setRollbackOnly(); - }).then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, databaseSelection, userSelection)) - .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue()))) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - verify(driver).session(eq(ReactiveSession.class), any(SessionConfig.class)); - - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - verify(transaction).rollback(); - verify(transaction, never()).commit(); - } - } -} diff --git a/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTests.java b/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTests.java new file mode 100644 index 000000000..01ef6d906 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2011-2025 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.core.transaction; + +import java.lang.reflect.Field; +import java.util.Set; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.neo4j.driver.Bookmark; +import org.neo4j.driver.Driver; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.reactivestreams.ReactiveSession; +import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.data.neo4j.core.DatabaseSelection; +import org.springframework.data.neo4j.core.UserSelection; +import org.springframework.data.neo4j.core.support.BookmarkManagerReference; +import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.reactive.TransactionalOperator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ReactiveNeo4jTransactionManagerTests { + + private DatabaseSelection databaseSelection = DatabaseSelection.byName("aDatabase"); + + private UserSelection userSelection = UserSelection.connectedUser(); + + @Mock + private Driver driver; + + @Mock + private ReactiveSession session; + + @Mock + private ReactiveTransaction transaction; + + @BeforeEach + void setUp() { + + given(this.driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).willReturn(this.session); + given(this.session.beginTransaction(any(TransactionConfig.class))).willReturn(Mono.just(this.transaction)); + given(this.transaction.rollback()).willReturn(Mono.empty()); + given(this.transaction.commit()).willReturn(Mono.empty()); + given(this.session.close()).willReturn(Mono.empty()); + } + + @Test + void shouldWorkWithoutSynchronizations() { + + Mono transactionMono = ReactiveNeo4jTransactionManager + .retrieveReactiveTransaction(this.driver, this.databaseSelection, this.userSelection); + + StepVerifier.create(transactionMono).verifyComplete(); + } + + @Nested + class BasedOnNeo4jTransactions { + + @Test + void shouldUseTxFromNeo4jTxManager() { + + ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager + .with(ReactiveNeo4jTransactionManagerTests.this.driver) + .withDatabaseSelectionProvider( + () -> Mono.just(ReactiveNeo4jTransactionManagerTests.this.databaseSelection)) + .build(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); + + transactionalOperator.execute( + transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction().doOnNext(tsm -> { + assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)).isTrue(); + transactionStatus.setRollbackOnly(); + }) + .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection))) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(ReactiveNeo4jTransactionManagerTests.this.driver).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + + verify(ReactiveNeo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(ReactiveNeo4jTransactionManagerTests.this.session).close(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction).rollback(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction, never()).commit(); + } + + @Test + void shouldParticipateInOngoingTransaction() { + + ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager + .with(ReactiveNeo4jTransactionManagerTests.this.driver) + .withDatabaseSelectionProvider( + () -> Mono.just(ReactiveNeo4jTransactionManagerTests.this.databaseSelection)) + .build(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); + + transactionalOperator.execute(outerStatus -> { + assertThat(outerStatus.isNewTransaction()).isTrue(); + outerStatus.setRollbackOnly(); + return transactionalOperator.execute(innerStatus -> { + assertThat(innerStatus.isNewTransaction()).isFalse(); + return ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection); + }) + .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection)); + }).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + + verify(ReactiveNeo4jTransactionManagerTests.this.driver).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + + verify(ReactiveNeo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(ReactiveNeo4jTransactionManagerTests.this.session).close(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction).rollback(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction, never()).commit(); + } + + @Test + void usesBookmarksCorrectly() throws Exception { + + ReactiveNeo4jTransactionManager txManager = ReactiveNeo4jTransactionManager + .with(ReactiveNeo4jTransactionManagerTests.this.driver) + .withDatabaseSelectionProvider( + () -> Mono.just(ReactiveNeo4jTransactionManagerTests.this.databaseSelection)) + .build(); + + AssertableBookmarkManager bookmarkManager = new AssertableBookmarkManager(); + injectBookmarkManager(txManager, bookmarkManager); + + Set bookmark = Set.of(new BookmarkForTesting("blubb")); + given(ReactiveNeo4jTransactionManagerTests.this.session.lastBookmarks()).willReturn(bookmark); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); + + transactionalOperator + .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction() + .doOnNext(tsm -> assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)) + .isTrue()) + .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection))) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(ReactiveNeo4jTransactionManagerTests.this.driver).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + verify(ReactiveNeo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + assertThat(bookmarkManager.getBookmarksCalled).isTrue(); + verify(ReactiveNeo4jTransactionManagerTests.this.session).close(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction).commit(); + assertThat(bookmarkManager.updateBookmarksCalled).containsEntry(bookmark, true); + } + + private void injectBookmarkManager(ReactiveNeo4jTransactionManager txManager, Neo4jBookmarkManager value) + throws NoSuchFieldException, IllegalAccessException { + Field bookmarkManager = ReactiveNeo4jTransactionManager.class.getDeclaredField("bookmarkManager"); + bookmarkManager.setAccessible(true); + bookmarkManager.set(txManager, new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, value)); + } + + } + + @Nested + class BasedOnOtherTransactions { + + @Test + void shouldSynchronizeWithExternalWithCommit() { + + R2dbcTransactionManager t = new R2dbcTransactionManager( + new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("test").build())); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(t); + + transactionalOperator + .execute(transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction() + .doOnNext(tsm -> assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)) + .isFalse()) + .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection)) + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() + .doOnNext(tsm -> assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)) + .isTrue()))) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(ReactiveNeo4jTransactionManagerTests.this.driver).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + + verify(ReactiveNeo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(ReactiveNeo4jTransactionManagerTests.this.session).close(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction).commit(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction, never()).rollback(); + } + + @Test + void shouldSynchronizeWithExternalWithRollback() { + + R2dbcTransactionManager t = new R2dbcTransactionManager( + new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("test").build())); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(t); + + transactionalOperator.execute( + transactionStatus -> TransactionSynchronizationManager.forCurrentTransaction().doOnNext(tsm -> { + assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)).isFalse(); + transactionStatus.setRollbackOnly(); + }) + .then(ReactiveNeo4jTransactionManager.retrieveReactiveTransaction( + ReactiveNeo4jTransactionManagerTests.this.driver, + ReactiveNeo4jTransactionManagerTests.this.databaseSelection, + ReactiveNeo4jTransactionManagerTests.this.userSelection)) + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() + .doOnNext( + tsm -> assertThat(tsm.hasResource(ReactiveNeo4jTransactionManagerTests.this.driver)) + .isTrue()))) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(ReactiveNeo4jTransactionManagerTests.this.driver).session(eq(ReactiveSession.class), + any(SessionConfig.class)); + + verify(ReactiveNeo4jTransactionManagerTests.this.session).beginTransaction(any(TransactionConfig.class)); + verify(ReactiveNeo4jTransactionManagerTests.this.session).close(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction).rollback(); + verify(ReactiveNeo4jTransactionManagerTests.this.transaction, never()).commit(); + } + + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java b/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java deleted file mode 100644 index 60ff6a0c6..000000000 --- a/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2011-2025 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.documentation; - -import java.util.Optional; - -import org.neo4j.driver.Driver; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.data.neo4j.core.DatabaseSelection; -import org.springframework.data.neo4j.core.DatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; - -// mock classes from Spring Security -class Authentication { - boolean isAuthenticated() { - return false; - } - - User getPrincipal() { - return null; - } -} - -class SecurityContext { - Authentication getAuthentication() { - return null; - } -} - -class SecurityContextHolder { - static SecurityContext getContext() { - return null; - } -} - -class User { - String getUsername() { - return null; - } -} - -/** - * @author Michael J. Simons - */ -// tag::faq.multidatabase[] -@Configuration -public class Neo4jConfig { - - // end::faq.multidatabase[] - /** - * This bean is only active in profile {@literal "selection-by-user"}. The {@link DatabaseSelectionProvider} created - * here uses Springs security context to retrieve the authenticated principal and extracts the username. Thus all - * requests will use a different database, depending on the user being logged into the application. - * - * @return A database name provider. - */ - @Profile("selection-by-user") - // tag::faq.multidatabase[] - @Bean - DatabaseSelectionProvider databaseSelectionProvider() { - - return () -> Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication) - .filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast) - .map(User::getUsername).map(DatabaseSelection::byName).orElseGet(DatabaseSelection::undecided); - } - // end::faq.multidatabase[] - - @Profile("multiple-transaction-manager") - @Configuration - static class MultipleTransactionManager { - - /** - * This is gonna be the default transaction manager, it's name corresponds with - * {@link Neo4jRepositoryConfigurationExtension#DEFAULT_TRANSACTION_MANAGER_BEAN_NAME}. - * - * @param driver The driver needed - * @param databaseNameProvider Whatever database name provider is configured - * @return A transaction manager - */ - @Bean - public Neo4jTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { - - return new Neo4jTransactionManager(driver, databaseNameProvider); - } - - /** - * A 2nd transaction manager for user with another database. - * - * @param driver The driver needed - * @return A transaction manager - */ - @Bean - public Neo4jTransactionManager transactionManagerForOtherDb(Driver driver) { - return new Neo4jTransactionManager(driver, - DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("otherDb")); - } - } - - // tag::faq.multidatabase[] -} -// end::faq.multidatabase[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java b/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java index ae95efce5..329326cf8 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java @@ -45,6 +45,7 @@ public class MovieEntity { // tag::mapping.relationship.properties[] @Relationship(type = "ACTED_IN", direction = Direction.INCOMING) // <.> private List actorsAndRoles = new ArrayList<>(); + // end::mapping.relationship.properties[] @Relationship(type = "DIRECTED", direction = Direction.INCOMING) @@ -60,22 +61,23 @@ public class MovieEntity { // end::mapping.annotations[] public String getTitle() { - return title; + return this.title; } public String getDescription() { - return description; + return this.description; } public List getActorsAndRoles() { - return actorsAndRoles; + return this.actorsAndRoles; } public List getDirectors() { - return directors; + return this.directors; } // tag::mapping.annotations[] // tag::faq.custom-query[] + } // end::mapping.annotations[] // end::faq.custom-query[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieRepository.java b/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieRepository.java index 1567c544a..a395741fd 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieRepository.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/domain/MovieRepository.java @@ -29,5 +29,6 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; public interface MovieRepository extends ReactiveNeo4jRepository { Mono findOneByTitle(String title); + } // end::getting.started[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/domain/PersonEntity.java b/src/test/java/org/springframework/data/neo4j/documentation/domain/PersonEntity.java index 12d2cd8ff..3ea222beb 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/domain/PersonEntity.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/domain/PersonEntity.java @@ -28,7 +28,8 @@ import org.springframework.data.neo4j.core.schema.Node; @Node("Person") public class PersonEntity { - @Id private final String name; + @Id + private final String name; private final Integer born; @@ -38,11 +39,11 @@ public class PersonEntity { } public Integer getBorn() { - return born; + return this.born; } public String getName() { - return name; + return this.name; } } diff --git a/src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java b/src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java index ac864285b..f0706506b 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java @@ -28,14 +28,14 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class Roles { - @RelationshipId - private Long id; - private final List roles; @TargetNode private final PersonEntity person; + @RelationshipId + private Long id; + public Roles(PersonEntity person, List roles) { this.person = person; this.roles = roles; @@ -43,19 +43,18 @@ public class Roles { // end::mapping.relationship.properties[] public Long getId() { - return id; + return this.id; } // tag::mapping.relationship.properties[] public List getRoles() { - return roles; + return this.roles; } @Override public String toString() { - return "Roles{" + - "id=" + id + - '}' + this.hashCode(); + return "Roles{" + "id=" + this.id + '}' + this.hashCode(); } + } // end::mapping.relationship.properties[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/Config.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/Config.java index e025fc04f..bfc0f1a97 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/Config.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/Config.java @@ -23,6 +23,7 @@ import java.util.Collection; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.config.AbstractNeo4jConfig; @@ -36,9 +37,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration // <1> @EnableNeo4jRepositories // <2> @EnableTransactionManagement // <3> -public class Config extends AbstractNeo4jConfig { // <4> +public class Config extends AbstractNeo4jConfig { + + // <4> @Bean + @Override public Driver driver() { // <5> return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret")); } @@ -50,6 +54,7 @@ public class Config extends AbstractNeo4jConfig { // <4> } // tag::java-config-imperative-short[] + } // end::java-config-imperative[] // end::java-config-imperative-short[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/CustomFragmentPostfix.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/CustomFragmentPostfix.java index 9cfc04a12..218a1d195 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/CustomFragmentPostfix.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/CustomFragmentPostfix.java @@ -23,5 +23,7 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; * Custom fragment postfix definition */ @EnableNeo4jRepositories(repositoryImplementationPostfix = "MyPostfix") -public class CustomFragmentPostfix {} +public class CustomFragmentPostfix { + +} // end::all[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/ReactiveConfig.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/ReactiveConfig.java index 03fb1c60a..aeddf73ef 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/ReactiveConfig.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/ReactiveConfig.java @@ -16,9 +16,11 @@ package org.springframework.data.neo4j.documentation.repositories; // tag::java-config-reactive[] + import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; @@ -34,8 +36,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; public class ReactiveConfig extends AbstractReactiveNeo4jConfig { @Bean + @Override public Driver driver() { return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret")); } + } // end::java-config-reactive[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java index f5e652fcb..fb67edfc2 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Set; import org.neo4j.driver.Value; + import org.springframework.context.annotation.Bean; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -44,7 +45,8 @@ public class MyCustomTypeConverter implements GenericConverter { if (MyCustomType.class.isAssignableFrom(sourceType.getType())) { // convert to Neo4j Driver Value return convertToNeo4jValue(source); - } else { + } + else { // convert to MyCustomType return convertToMyCustomType(source); } @@ -59,8 +61,6 @@ public class MyCustomTypeConverter implements GenericConverter { } // end::custom-converter.neo4jConversions[] - private static class MyCustomType {} - private Object convertToNeo4jValue(Object source) { return null; } @@ -68,6 +68,11 @@ public class MyCustomTypeConverter implements GenericConverter { private Object convertToMyCustomType(Object source) { return null; } + + private static final class MyCustomType { + + } // tag::custom-converter.implementation[] + } // end::custom-converter.implementation[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/CustomQueriesIT.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/CustomQueriesIT.java index 7c4635734..3709d75c8 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/CustomQueriesIT.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/CustomQueriesIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.documentation.repositories.custom_queries; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -26,6 +24,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,6 +43,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -52,30 +53,6 @@ class CustomQueriesIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - // tag::custom-queries-test[] - @Test - void customRepositoryFragmentsShouldWork( - @Autowired PersonRepository people, - @Autowired MovieRepository movies - ) { - - PersonEntity meg = people.findById("Meg Ryan").get(); - PersonEntity kevin = people.findById("Kevin Bacon").get(); - - List moviesBetweenMegAndKevin = movies. - findMoviesAlongShortestPath(meg, kevin); - assertThat(moviesBetweenMegAndKevin).isNotEmpty(); - - Collection relatedPeople = movies - .findRelationsToMovie(moviesBetweenMegAndKevin.get(0)); - assertThat(relatedPeople).isNotEmpty(); - - assertThat(movies.deleteGraph()).isGreaterThan(0); - assertThat(movies.findAll()).isEmpty(); - assertThat(people.findAll()).isEmpty(); - } - // end::custom-queries-test[] - @BeforeAll static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) throws IOException { @@ -86,7 +63,28 @@ class CustomQueriesIT { } } + // tag::custom-queries-test[] + @Test + void customRepositoryFragmentsShouldWork(@Autowired PersonRepository people, @Autowired MovieRepository movies) { + + PersonEntity meg = people.findById("Meg Ryan").get(); + PersonEntity kevin = people.findById("Kevin Bacon").get(); + + List moviesBetweenMegAndKevin = movies.findMoviesAlongShortestPath(meg, kevin); + assertThat(moviesBetweenMegAndKevin).isNotEmpty(); + + Collection relatedPeople = movies + .findRelationsToMovie(moviesBetweenMegAndKevin.get(0)); + assertThat(relatedPeople).isNotEmpty(); + + assertThat(movies.deleteGraph()).isGreaterThan(0); + assertThat(movies.findAll()).isEmpty(); + assertThat(people.findAll()).isEmpty(); + } + // end::custom-queries-test[] + interface PersonRepository extends Neo4jRepository { + } @Configuration @@ -106,20 +104,24 @@ class CustomQueriesIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResults.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResults.java new file mode 100644 index 000000000..a09455d95 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResults.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +import java.util.List; + +import org.springframework.data.neo4j.documentation.domain.MovieEntity; +import org.springframework.data.neo4j.documentation.domain.PersonEntity; +import org.springframework.transaction.annotation.Transactional; + +interface DomainResults { + + @Transactional(readOnly = true) + List findMoviesAlongShortestPath(PersonEntity from, PersonEntity to); + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResultsImpl.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResultsImpl.java new file mode 100644 index 000000000..53b1d53d5 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/DomainResultsImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.neo4j.cypherdsl.core.Cypher; + +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.documentation.domain.MovieEntity; +import org.springframework.data.neo4j.documentation.domain.PersonEntity; + +import static org.neo4j.cypherdsl.core.Cypher.anyNode; +import static org.neo4j.cypherdsl.core.Cypher.listWith; +import static org.neo4j.cypherdsl.core.Cypher.name; +import static org.neo4j.cypherdsl.core.Cypher.node; +import static org.neo4j.cypherdsl.core.Cypher.parameter; +import static org.neo4j.cypherdsl.core.Cypher.shortestPath; + +class DomainResultsImpl implements DomainResults { + + private final Neo4jTemplate neo4jTemplate; // <.> + + DomainResultsImpl(Neo4jTemplate neo4jTemplate) { + this.neo4jTemplate = neo4jTemplate; + } + + @Override + public List findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) { + + var p1 = node("Person").withProperties("name", parameter("person1")); + var p2 = node("Person").withProperties("name", parameter("person2")); + var shortestPath = shortestPath("p").definedBy(p1.relationshipBetween(p2).unbounded()); + var p = shortestPath.getRequiredSymbolicName(); + var statement = Cypher.match(shortestPath) + .with(p, listWith(name("n")).in(Cypher.nodes(shortestPath)) + .where(anyNode().named("n").hasLabels("Movie")) + .returning() + .as("mn")) + .unwind(name("mn")) + .as("m") + .with(p, name("m")) + .match(node("Person").named("d").relationshipTo(anyNode("m"), "DIRECTED").named("r")) + .returning(p, Cypher.collect(name("r")), Cypher.collect(name("d"))) + .build(); + + Map parameters = new HashMap<>(); + parameters.put("person1", from.getName()); + parameters.put("person2", to.getName()); + return this.neo4jTemplate.findAll(statement, parameters, MovieEntity.class); // <.> + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractions.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractions.java new file mode 100644 index 000000000..d9d53f2ae --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractions.java @@ -0,0 +1,22 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +interface LowlevelInteractions { + + int deleteGraph(); + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractionsImpl.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractionsImpl.java new file mode 100644 index 000000000..85ff17e53 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/LowlevelInteractionsImpl.java @@ -0,0 +1,40 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.summary.SummaryCounters; + +class LowlevelInteractionsImpl implements LowlevelInteractions { + + private final Driver driver; // <.> + + LowlevelInteractionsImpl(Driver driver) { + this.driver = driver; + } + + @Override + public int deleteGraph() { + + try (Session session = this.driver.session()) { + SummaryCounters counters = session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) // <.> + .counters(); + return counters.nodesDeleted() + counters.relationshipsDeleted(); + } + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java index 88057e532..b6ea44585 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java @@ -15,166 +15,10 @@ */ package org.springframework.data.neo4j.documentation.repositories.custom_queries; -// tag::domain-results-impl[] -import static org.neo4j.cypherdsl.core.Cypher.anyNode; -import static org.neo4j.cypherdsl.core.Cypher.listWith; -import static org.neo4j.cypherdsl.core.Cypher.name; -import static org.neo4j.cypherdsl.core.Cypher.node; -import static org.neo4j.cypherdsl.core.Cypher.parameter; -import static org.neo4j.cypherdsl.core.Cypher.shortestPath; - -// end::domain-results-impl[] -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -// tag::domain-results-impl[] -import org.neo4j.cypherdsl.core.Cypher; - -// end::domain-results-impl[] - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.summary.SummaryCounters; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.documentation.domain.MovieEntity; -import org.springframework.data.neo4j.documentation.domain.PersonEntity; -// tag::aggregating-interface[] import org.springframework.data.neo4j.repository.Neo4jRepository; -// end::aggregating-interface[] -import org.springframework.transaction.annotation.Transactional; +public interface MovieRepository + extends Neo4jRepository, DomainResults, NonDomainResults, LowlevelInteractions { -/** - * Example for a Spring Data Neo4j repository using several fragments to compose functionality. - * - * @author Michael J. Simons - */ -// tag::aggregating-interface[] -public interface MovieRepository extends Neo4jRepository, - DomainResults, - NonDomainResults, - LowlevelInteractions { } -// end::aggregating-interface[] - -// tag::domain-results[] -interface DomainResults { - - @Transactional(readOnly = true) - List findMoviesAlongShortestPath(PersonEntity from, PersonEntity to); -} -// end::domain-results[] - -// tag::domain-results-impl[] -class DomainResultsImpl implements DomainResults { - - private final Neo4jTemplate neo4jTemplate; // <.> - - DomainResultsImpl(Neo4jTemplate neo4jTemplate) { - this.neo4jTemplate = neo4jTemplate; - } - - @Override - public List findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) { - - var p1 = node("Person").withProperties("name", parameter("person1")); - var p2 = node("Person").withProperties("name", parameter("person2")); - var shortestPath = shortestPath("p").definedBy( - p1.relationshipBetween(p2).unbounded() - ); - var p = shortestPath.getRequiredSymbolicName(); - var statement = Cypher.match(shortestPath) - .with(p, listWith(name("n")) - .in(Cypher.nodes(shortestPath)) - .where(anyNode().named("n").hasLabels("Movie")).returning().as("mn") - ) - .unwind(name("mn")).as("m") - .with(p, name("m")) - .match(node("Person").named("d") - .relationshipTo(anyNode("m"), "DIRECTED").named("r") - ) - .returning(p, Cypher.collect(name("r")), Cypher.collect(name("d"))) - .build(); - - Map parameters = new HashMap<>(); - parameters.put("person1", from.getName()); - parameters.put("person2", to.getName()); - return neo4jTemplate.findAll(statement, parameters, MovieEntity.class); // <.> - } -} -// end::domain-results-impl[] - -// tag::non-domain-results[] -interface NonDomainResults { - - class Result { // <.> - public final String name; - - public final String typeOfRelation; - - Result(String name, String typeOfRelation) { - this.name = name; - this.typeOfRelation = typeOfRelation; - } - } - - @Transactional(readOnly = true) - Collection findRelationsToMovie(MovieEntity movie); // <.> -} -// end::non-domain-results[] - -// tag::non-domain-results-impl[] -class NonDomainResultsImpl implements NonDomainResults { - - private final Neo4jClient neo4jClient; // <.> - - NonDomainResultsImpl(Neo4jClient neo4jClient) { - this.neo4jClient = neo4jClient; - } - - @Override - public Collection findRelationsToMovie(MovieEntity movie) { - return this.neo4jClient - .query("" - + "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) " - + "RETURN people.name AS name, " - + " Type(relatedTo) as typeOfRelation" - ) // <.> - .bind(movie.getTitle()).to("title") // <.> - .fetchAs(Result.class) // <.> - .mappedBy((typeSystem, record) -> new Result(record.get("name").asString(), - record.get("typeOfRelation").asString())) // <.> - .all(); // <.> - } -} -// end::non-domain-results-impl[] - -// tag::lowlevel-interactions[] -interface LowlevelInteractions { - - int deleteGraph(); -} - -class LowlevelInteractionsImpl implements LowlevelInteractions { - - private final Driver driver; // <.> - - LowlevelInteractionsImpl(Driver driver) { - this.driver = driver; - } - - @Override - public int deleteGraph() { - - try (Session session = driver.session()) { - SummaryCounters counters = session - .executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) // <.> - .counters(); - return counters.nodesDeleted() + counters.relationshipsDeleted(); - } - } -} -// end::lowlevel-interactions[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResults.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResults.java new file mode 100644 index 000000000..02b4c931f --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResults.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +import java.util.Collection; + +import org.springframework.data.neo4j.documentation.domain.MovieEntity; +import org.springframework.transaction.annotation.Transactional; + +interface NonDomainResults { + + @Transactional(readOnly = true) + Collection findRelationsToMovie(MovieEntity movie); // <.> + + class Result { + + // <.> + public final String name; + + public final String typeOfRelation; + + Result(String name, String typeOfRelation) { + this.name = name; + this.typeOfRelation = typeOfRelation; + } + + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResultsImpl.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResultsImpl.java new file mode 100644 index 000000000..36f4e444c --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/NonDomainResultsImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright 2011-2025 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.documentation.repositories.custom_queries; + +import java.util.Collection; + +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.documentation.domain.MovieEntity; + +class NonDomainResultsImpl implements NonDomainResults { + + private final Neo4jClient neo4jClient; // <.> + + NonDomainResultsImpl(Neo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; + } + + @Override + public Collection findRelationsToMovie(MovieEntity movie) { + return this.neo4jClient + .query("" + "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) " + "RETURN people.name AS name, " + + " Type(relatedTo) as typeOfRelation") // <.> + .bind(movie.getTitle()) + .to("title") // <.> + .fetchAs(Result.class) // <.> + .mappedBy((typeSystem, record) -> new Result(record.get("name").asString(), + record.get("typeOfRelation").asString())) // <.> + .all(); // <.> + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java index bc3c53908..36cea5a4c 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java @@ -45,6 +45,7 @@ public interface ARepository extends Neo4jRepository { @Query("MATCH (a:AnAggregateRoot) WHERE a.name = :#{#pt1 + #pt2} RETURN a") Optional findByCustomQueryWithSpEL(String pt1, String pt2); // tag::standard-parameter[] + } // end::standard-parameter[] // end::spel[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/AnAggregateRoot.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/AnAggregateRoot.java index 688b811f9..9777d207c 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/AnAggregateRoot.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/AnAggregateRoot.java @@ -33,24 +33,25 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class AnAggregateRoot { - @Id private final String name; - - private String someOtherValue; + @Id + private final String name; @Transient // <1> private final Collection events = new ArrayList<>(); + private String someOtherValue; + public AnAggregateRoot(String name) { this.name = name; } // end::domain-events[] public String getName() { - return name; + return this.name; } public String getSomeOtherValue() { - return someOtherValue; + return this.someOtherValue; } // tag::domain-events[] @@ -62,7 +63,7 @@ public class AnAggregateRoot { @DomainEvents // <3> Collection domainEvents() { - return Collections.unmodifiableCollection(events); + return Collections.unmodifiableCollection(this.events); } @AfterDomainEventPublication @@ -70,5 +71,6 @@ public class AnAggregateRoot { void callbackMethod() { this.events.clear(); } + } // end::domain-events[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsApplication.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsApplication.java index 924da6264..67a3c6d95 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsApplication.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsApplication.java @@ -27,10 +27,12 @@ public class DomainEventsApplication { // tag::domain-events[] @Bean ApplicationListener someEventListener() { - return event -> log("someOtherValue changed from '" + event.getOldValue() + "' to '" + event.getNewValue() + "' at " - + event.getChangeHappenedAt()); + return event -> log("someOtherValue changed from '" + event.getOldValue() + "' to '" + event.getNewValue() + + "' at " + event.getChangeHappenedAt()); } // end::domain-events[] - void log(String message) {} + void log(String message) { + } + } diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTests.java similarity index 83% rename from src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java rename to src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTests.java index 266b78cb7..8958955cd 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTests.java @@ -15,27 +15,28 @@ */ package org.springframework.data.neo4j.documentation.repositories.domain_events; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Optional; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; +import static org.assertj.core.api.Assertions.assertThat; + /** * Basic tests for domain events (and documentation). */ @Disabled @TestPropertySource("foo=The Root") // tag::domain-events[] -public class DomainEventsTest { +public class DomainEventsTests { private final ARepository aRepository; @Autowired - public DomainEventsTest(ARepository aRepository) { + public DomainEventsTests(ARepository aRepository) { this.aRepository = aRepository; this.aRepository.save(new AnAggregateRoot("The Root")); } @@ -54,16 +55,17 @@ public class DomainEventsTest { @Test void customQueryShouldWork() { - Optional optionalAggregate = aRepository.findByCustomQuery("The Root"); + Optional optionalAggregate = this.aRepository.findByCustomQuery("The Root"); assertThat(optionalAggregate).isPresent(); - optionalAggregate = aRepository.findByCustomQueryWithSpEL("The ", "Root"); + optionalAggregate = this.aRepository.findByCustomQueryWithSpEL("The ", "Root"); assertThat(optionalAggregate).isPresent(); - optionalAggregate = aRepository.findByCustomQueryWithPropertyPlaceholder(); + optionalAggregate = this.aRepository.findByCustomQueryWithPropertyPlaceholder(); assertThat(optionalAggregate).isPresent(); } // tag::domain-events[] + } // end::domain-events[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/SomeEvent.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/SomeEvent.java index 435eba94a..1bf4e415d 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/SomeEvent.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/SomeEvent.java @@ -23,7 +23,9 @@ import org.springframework.context.ApplicationEvent; * Some example event. */ // tag::domain-events[] -public class SomeEvent extends ApplicationEvent { // <1> +public class SomeEvent extends ApplicationEvent { + + // <1> private final LocalDateTime changeHappenedAt = LocalDateTime.now(); @@ -41,16 +43,17 @@ public class SomeEvent extends ApplicationEvent { // <1> // end::domain-events[] public LocalDateTime getChangeHappenedAt() { - return changeHappenedAt; + return this.changeHappenedAt; } public String getOldValue() { - return oldValue; + return this.oldValue; } public String getNewValue() { - return newValue; + return this.newValue; } // tag::domain-events[] + } // end::domain-events[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java b/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTests.java similarity index 85% rename from src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java rename to src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTests.java index 2c9b33e78..7d1d339f3 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTests.java @@ -15,16 +15,15 @@ */ package org.springframework.data.neo4j.documentation.spring_boot; -// tag::faq.template-reactive-pt1[] - -import reactor.test.StepVerifier; - import java.util.Collections; -// end::faq.template-reactive-pt1[] import org.junit.jupiter.api.Disabled; -// tag::faq.template-reactive-pt1[] import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.documentation.domain.MovieEntity; @@ -32,23 +31,16 @@ import org.springframework.data.neo4j.documentation.domain.PersonEntity; import org.springframework.data.neo4j.documentation.domain.Roles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.Neo4jContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -// end::faq.template-reactive-pt1[] /** * @author Michael J. Simons */ @Disabled -// tag::faq.template-reactive-pt1[] @Testcontainers -// end::faq.template-reactive-pt1[] -// tag::faq.template-reactive-pt2[] -class ReactiveTemplateExampleTest { +class ReactiveTemplateExampleTests { - @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:5"); + @Container + private static Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:5"); @DynamicPropertySource static void neo4jProperties(DynamicPropertyRegistry registry) { @@ -71,9 +63,10 @@ class ReactiveTemplateExampleTest { StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete(); StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn)) - .expectNext(1931).verifyComplete(); + .expectNext(1931) + .verifyComplete(); StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete(); } + } -// end::faq.template-reactive-pt2[] diff --git a/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java b/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTests.java similarity index 80% rename from src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java rename to src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTests.java index c1be3d259..67dde9150 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTests.java @@ -15,35 +15,23 @@ */ package org.springframework.data.neo4j.documentation.spring_boot; -// tag::faq.template-imperative-pt1[] - -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import java.util.Optional; -// end::faq.template-imperative-pt1[] import org.junit.jupiter.api.BeforeEach; -// tag::faq.template-imperative-pt1[] import org.junit.jupiter.api.Test; -// end::faq.template-imperative-pt1[] import org.neo4j.driver.Driver; -// tag::faq.template-imperative-pt1[] + import org.springframework.beans.factory.annotation.Autowired; -// end::faq.template-imperative-pt1[] import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; -// tag::faq.template-imperative-pt1[] import org.springframework.data.neo4j.core.Neo4jTemplate; -// end::faq.template-imperative-pt1[] import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; -// tag::faq.template-imperative-pt1[] import org.springframework.data.neo4j.documentation.domain.MovieEntity; import org.springframework.data.neo4j.documentation.domain.PersonEntity; import org.springframework.data.neo4j.documentation.domain.Roles; -// end::faq.template-imperative-pt1[] import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; @@ -51,31 +39,27 @@ import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -// tag::faq.template-imperative-pt1[] -// end::faq.template-imperative-pt1[] +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons */ @Neo4jIntegrationTest -// tag::faq.template-imperative-pt2[] -public class TemplateExampleTest { - - // end::faq.template-imperative-pt2[] +public class TemplateExampleTests { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; @BeforeEach void setup(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { - try (var session = driver.session(bookmarkCapture.createSessionConfig()); var transaction = session.beginTransaction()) { + try (var session = driver.session(bookmarkCapture.createSessionConfig()); + var transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n").consume(); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); } } - // tag::faq.template-imperative-pt2[] @Test void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) { @@ -89,9 +73,7 @@ public class TemplateExampleTest { movie.getActorsAndRoles().add(roles2); MovieEntity result = neo4jTemplate.save(movie); - // end::mapping.relationship.properties[] assertThat(result.getActorsAndRoles()).allSatisfy(relationship -> assertThat(relationship.getId()).isNotNull()); - // tag::mapping.relationship.properties[] Optional person = neo4jTemplate.findById("Dean Jones", PersonEntity.class); assertThat(person).map(PersonEntity::getBorn).hasValue(1931); @@ -99,34 +81,36 @@ public class TemplateExampleTest { assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L); } - // end::faq.template-imperative-pt2[] @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } - // tag::faq.template-imperative-pt2[] + } -// end::faq.template-imperative-pt2[] diff --git a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/DatabaseInitializer.java b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/DatabaseInitializer.java index 3e93829ea..41347257d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/DatabaseInitializer.java +++ b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/DatabaseInitializer.java @@ -20,6 +20,7 @@ import java.io.UncheckedIOException; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.InitializingBean; import org.springframework.data.neo4j.integration.movies.shared.CypherUtils; @@ -36,11 +37,13 @@ public final class DatabaseInitializer implements InitializingBean { @Override public void afterPropertiesSet() { - try (Session session = driver.session()) { + try (Session session = this.driver.session()) { session.run("MATCH (n) DETACH DELETE n").consume(); CypherUtils.loadCypherFromResource("/data/movies.cypher", session); - } catch (IOException e) { - throw new UncheckedIOException(e); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/Person.java b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/Person.java index 44639d206..d8e82ec9e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/Person.java +++ b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/Person.java @@ -30,4 +30,5 @@ public class Person { private String id; private String name; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/imperative/NoopBookmarkmanagerIT.java b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/imperative/NoopBookmarkmanagerIT.java index 4f6227be1..89901c51b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/imperative/NoopBookmarkmanagerIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/imperative/NoopBookmarkmanagerIT.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.integration.bookmarks.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -31,6 +26,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.neo4j.driver.Driver; import org.neo4j.driver.SessionConfig; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -49,6 +45,11 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Michael J. Simons */ @@ -57,6 +58,34 @@ public class NoopBookmarkmanagerIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + @Test + void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) + throws ExecutionException, InterruptedException { + + var movies = personService.getMoviesByActorNameLike("Bill"); + assertThat(movies).hasSize(5); + var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class); + verify(driver, times(5)).session(any(), sessionConfigCaptor.capture()); + assertThat(sessionConfigCaptor.getAllValues()).allMatch(cfg -> { + var bookmarks = new ArrayList<>(); + if (cfg.bookmarks() != null) { + cfg.bookmarks().forEach(bookmarks::add); + } + return bookmarks.isEmpty(); + }); + } + + interface PersonRepository extends Neo4jRepository { + + @Async + @Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name") + CompletableFuture> findMatchingNames(String name); + + @Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title") + CompletableFuture> getPersonMovies(String name); + + } + @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @EnableTransactionManagement @@ -70,6 +99,7 @@ public class NoopBookmarkmanagerIT { } @Bean + @Override public Driver driver() { var driver = neo4jConnectionSupport.getDriver(); return Mockito.spy(driver); @@ -84,55 +114,33 @@ public class NoopBookmarkmanagerIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } - } - @Test - void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) throws ExecutionException, InterruptedException { - - var movies = personService.getMoviesByActorNameLike("Bill"); - assertThat(movies).hasSize(5); - var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class); - verify(driver, times(5)).session(any(), sessionConfigCaptor.capture()); - assertThat(sessionConfigCaptor.getAllValues()) - .allMatch(cfg -> { - var bookmarks = new ArrayList<>(); - if (cfg.bookmarks() != null) { - cfg.bookmarks().forEach(bookmarks::add); - } - return bookmarks.isEmpty(); - }); - } - - interface PersonRepository extends Neo4jRepository { - - @Async - @Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name") - CompletableFuture> findMatchingNames(String name); - - @Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title") - CompletableFuture> getPersonMovies(String name); } @Service static class PersonService { + private final PersonRepository personRepository; PersonService(PersonRepository personRepository) { this.personRepository = personRepository; } - public List getMoviesByActorNameLike(String namePattern) throws ExecutionException, InterruptedException { + List getMoviesByActorNameLike(String namePattern) throws ExecutionException, InterruptedException { - CompletableFuture> completableFutureCompletableFuture = personRepository.findMatchingNames(namePattern) - .thenCompose(names -> { - List result = Collections.synchronizedList(new ArrayList()); - var futures = names.stream().map(personRepository::getPersonMovies) - .map(cf -> cf.thenAccept(result::addAll)) - .toArray(CompletableFuture[]::new); - return CompletableFuture.allOf(futures) - .thenApply(__ -> result); - }); + CompletableFuture> completableFutureCompletableFuture = this.personRepository + .findMatchingNames(namePattern) + .thenCompose(names -> { + List result = Collections.synchronizedList(new ArrayList()); + var futures = names.stream() + .map(this.personRepository::getPersonMovies) + .map(cf -> cf.thenAccept(result::addAll)) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures).thenApply(__ -> result); + }); return completableFutureCompletableFuture.get(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/reactive/ReactiveNoopBookmarkmanagerIT.java b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/reactive/ReactiveNoopBookmarkmanagerIT.java index 127636ae3..c481ec018 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/bookmarks/reactive/ReactiveNoopBookmarkmanagerIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/bookmarks/reactive/ReactiveNoopBookmarkmanagerIT.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.integration.bookmarks.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -30,6 +25,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.neo4j.driver.Driver; import org.neo4j.driver.SessionConfig; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -46,9 +45,10 @@ import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Michael J. Simons @@ -59,6 +59,38 @@ public class ReactiveNoopBookmarkmanagerIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + @Test + void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) { + + AtomicReference> result = new AtomicReference<>(); + personService.getMoviesByActorNameLike("Bill") + .as(StepVerifier::create) + .consumeNextWith(result::set) + .verifyComplete(); + + assertThat(result).hasValueSatisfying(movies -> assertThat(movies).hasSize(5)); + + var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class); + verify(driver, times(5)).session(any(), sessionConfigCaptor.capture()); + assertThat(sessionConfigCaptor.getAllValues()).allMatch(cfg -> { + var bookmarks = new ArrayList<>(); + if (cfg.bookmarks() != null) { + cfg.bookmarks().forEach(bookmarks::add); + } + return bookmarks.isEmpty(); + }); + } + + interface PersonRepository extends ReactiveNeo4jRepository { + + @Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name") + Flux findMatchingNames(String name); + + @Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title") + Flux getPersonMovies(String name); + + } + @Configuration @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) @EnableTransactionManagement @@ -71,6 +103,7 @@ public class ReactiveNoopBookmarkmanagerIT { } @Bean + @Override public Driver driver() { var driver = neo4jConnectionSupport.getDriver(); return Mockito.spy(driver); @@ -85,53 +118,24 @@ public class ReactiveNoopBookmarkmanagerIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } - } - @Test - void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) { - - AtomicReference> result = new AtomicReference<>(); - personService.getMoviesByActorNameLike("Bill") - .as(StepVerifier::create) - .consumeNextWith(result::set) - .verifyComplete(); - - assertThat(result) - .hasValueSatisfying(movies -> assertThat(movies).hasSize(5)); - - var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class); - verify(driver, times(5)).session(any(), sessionConfigCaptor.capture()); - assertThat(sessionConfigCaptor.getAllValues()) - .allMatch(cfg -> { - var bookmarks = new ArrayList<>(); - if (cfg.bookmarks() != null) { - cfg.bookmarks().forEach(bookmarks::add); - } - return bookmarks.isEmpty(); - }); - } - - interface PersonRepository extends ReactiveNeo4jRepository { - - @Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name") - Flux findMatchingNames(String name); - - @Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title") - Flux getPersonMovies(String name); } @Service static class PersonService { + private final PersonRepository personRepository; PersonService(PersonRepository personRepository) { this.personRepository = personRepository; } - public Mono> getMoviesByActorNameLike(String namePattern) { - return personRepository.findMatchingNames(namePattern) - .flatMap(personRepository::getPersonMovies, 2) - .collectList(); + Mono> getMoviesByActorNameLike(String namePattern) { + return this.personRepository.findMatchingNames(namePattern) + .flatMap(this.personRepository::getPersonMovies, 2) + .collectList(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java index 2f14a4fd2..2f8561649 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.cascading; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,15 +22,18 @@ import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.neo4j.driver.Driver; import org.neo4j.driver.types.TypeSystem; + import org.springframework.beans.factory.annotation.Autowired; +import static org.assertj.core.api.Assertions.assertThat; + abstract class AbstractCascadingTestBase { + static Map, String> EXISTING_IDS = new HashMap<>(); + @Autowired Driver driver; - static Map, String> EXISTING_IDS = new HashMap<>(); - @BeforeAll static void clean(@Autowired Driver driver) { @@ -76,52 +77,55 @@ abstract class AbstractCascadingTestBase { } } - void assertAllRelationshipsHaveBeenCreated(T instance) { var type = instance.getClass(); - try (var session = driver.session()) { - var result = session.run(""" - MATCH (p:%s WHERE %s) - MATCH (p) -[:HAS_SINGLE_CUI]-> (sCUI) - MATCH (p) -[:HAS_SINGLE_CUE]-> (sCUE) - MATCH (p) -[:HAS_MANY_CUI]-> (mCUI) - MATCH (p) -[:HAS_SINGLE_CVI]-> (sCVI {version: 0}) - MATCH (p) -[:HAS_SINGLE_CVE]-> (sCVE {version: 0}) - MATCH (p) -[:HAS_MANY_CVI]-> (mCVI {version: 0}) - MATCH (sCUI) -[:HAS_NESTED_CHILDREN]-> (nc1) - MATCH (mCUI) -[:HAS_NESTED_CHILDREN]-> (nc2) - RETURN p, sCUI, sCUE, collect(DISTINCT mCUI) AS mCUI, collect(DISTINCT nc1) AS nc1, collect(DISTINCT nc2) AS nc2, - sCVI, sCVE, collect(DISTINCT mCVI) AS mCVI - """.formatted(type.getSimpleName(), instance instanceof ExternalId ? "p.id = $id" : "elementId(p) = $id"), Map.of("id", instance.getId())) - .list(); + try (var session = this.driver.session()) { + var result = session + .run(""" + MATCH (p:%s WHERE %s) + MATCH (p) -[:HAS_SINGLE_CUI]-> (sCUI) + MATCH (p) -[:HAS_SINGLE_CUE]-> (sCUE) + MATCH (p) -[:HAS_MANY_CUI]-> (mCUI) + MATCH (p) -[:HAS_SINGLE_CVI]-> (sCVI {version: 0}) + MATCH (p) -[:HAS_SINGLE_CVE]-> (sCVE {version: 0}) + MATCH (p) -[:HAS_MANY_CVI]-> (mCVI {version: 0}) + MATCH (sCUI) -[:HAS_NESTED_CHILDREN]-> (nc1) + MATCH (mCUI) -[:HAS_NESTED_CHILDREN]-> (nc2) + RETURN p, sCUI, sCUE, collect(DISTINCT mCUI) AS mCUI, collect(DISTINCT nc1) AS nc1, collect(DISTINCT nc2) AS nc2, + sCVI, sCVE, collect(DISTINCT mCVI) AS mCVI + """ + .formatted(type.getSimpleName(), + (instance instanceof ExternalId) ? "p.id = $id" : "elementId(p) = $id"), + Map.of("id", instance.getId())) + .list(); - assertThat(result).hasSize(1).element(0) - .satisfies(r -> { - if (instance instanceof Versioned) { - assertThat(r.get("p").asNode().get("version").asLong()).isZero(); - } - if (instance instanceof ExternalId) { - assertThat(r.get("p").asNode().get("id").asString()).isEqualTo(instance.getId()); - } else { - assertThat(r.get("p").asNode().elementId()).isEqualTo(instance.getId()); - } - assertThat(r.get("sCUI").hasType(TypeSystem.getDefault().NODE())).isTrue(); - assertThat(r.get("sCUE").hasType(TypeSystem.getDefault().NODE())).isTrue(); - assertThat(r.get("mCUI").asList(v -> v.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Parent.cUI1", "Parent.cUI2"); - assertThat(r.get("nc1").asList(v -> v.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Parent.singleCUI.cc1", "Parent.singleCUI.cc2"); - assertThat(r.get("nc2").asList(v -> v.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Parent.cUI1.cc1", "Parent.cUI1.cc2", "Parent.cUI2.cc1", "Parent.cUI2.cc2"); - assertThat(r.get("sCVI").asNode().get("version").asLong()).isZero(); - assertThat(r.get("sCVE").asNode().get("version").asLong()).isZero(); - assertThat(r.get("mCVI").asList(v -> { - var node = v.asNode(); - return node.get("name").asString() + "." + node.get("version").asLong(); - })) - .containsExactlyInAnyOrder("Parent.cVI1.0", "Parent.cVI2.0"); - }); + assertThat(result).hasSize(1).element(0).satisfies(r -> { + if (instance instanceof Versioned) { + assertThat(r.get("p").asNode().get("version").asLong()).isZero(); + } + if (instance instanceof ExternalId) { + assertThat(r.get("p").asNode().get("id").asString()).isEqualTo(instance.getId()); + } + else { + assertThat(r.get("p").asNode().elementId()).isEqualTo(instance.getId()); + } + assertThat(r.get("sCUI").hasType(TypeSystem.getDefault().NODE())).isTrue(); + assertThat(r.get("sCUE").hasType(TypeSystem.getDefault().NODE())).isTrue(); + assertThat(r.get("mCUI").asList(v -> v.asNode().get("name").asString())) + .containsExactlyInAnyOrder("Parent.cUI1", "Parent.cUI2"); + assertThat(r.get("nc1").asList(v -> v.asNode().get("name").asString())) + .containsExactlyInAnyOrder("Parent.singleCUI.cc1", "Parent.singleCUI.cc2"); + assertThat(r.get("nc2").asList(v -> v.asNode().get("name").asString())).containsExactlyInAnyOrder( + "Parent.cUI1.cc1", "Parent.cUI1.cc2", "Parent.cUI2.cc1", "Parent.cUI2.cc2"); + assertThat(r.get("sCVI").asNode().get("version").asLong()).isZero(); + assertThat(r.get("sCVE").asNode().get("version").asLong()).isZero(); + assertThat(r.get("mCVI").asList(v -> { + var node = v.asNode(); + return node.get("name").asString() + "." + node.get("version").asLong(); + })).containsExactlyInAnyOrder("Parent.cVI1.0", "Parent.cVI2.0"); + }); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java index 867cbd30a..bfe3d4464 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java @@ -39,10 +39,7 @@ public class CUE implements ExternalId { public CUE(String name) { this.name = name; - this.nested = List.of( - new CUE(name + ".cc1", List.of()), - new CUE(name + ".cc2", List.of()) - ); + this.nested = List.of(new CUE(name + ".cc1", List.of()), new CUE(name + ".cc2", List.of())); } @PersistenceCreator @@ -52,14 +49,15 @@ public class CUE implements ExternalId { } public String getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java index e4344c603..ea381d616 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java @@ -38,10 +38,7 @@ public class CUI { public CUI(String name) { this.name = name; - this.nested = List.of( - new CUI(name + ".cc1", List.of()), - new CUI(name + ".cc2", List.of()) - ); + this.nested = List.of(new CUI(name + ".cc1", List.of()), new CUI(name + ".cc2", List.of())); } @PersistenceCreator @@ -51,11 +48,11 @@ public class CUI { } public String getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -63,6 +60,7 @@ public class CUI { } public List getNested() { - return nested; + return this.nested; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java index 9e6ee5a33..e87ca4b3a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java @@ -39,14 +39,16 @@ public class CVE implements Versioned, ExternalId { } public String getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } + @Override public Long getVersion() { - return version; + return this.version; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java index fbeba72a0..90e1ea93b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java @@ -38,14 +38,16 @@ public class CVI implements Versioned { } public String getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } + @Override public Long getVersion() { - return version; + return this.version; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java index 009cdef7c..60979f36c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java @@ -15,14 +15,13 @@ */ package org.springframework.data.neo4j.integration.cascading; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.reflect.InvocationTargetException; import java.util.List; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.junitpioneer.jupiter.cartesian.CartesianTest.Values; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -33,34 +32,22 @@ import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + @Neo4jIntegrationTest @Import(CascadingIT.Config.class) class CascadingIT extends AbstractCascadingTestBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - @EnableTransactionManagement - @ComponentScan - static class Config extends Neo4jImperativeTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } - @Autowired Neo4jTemplate template; @CartesianTest void updatesMustNotCascade( - @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type, - @Values(booleans = {true, false}) boolean single) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + @Values(classes = { PUI.class, PUE.class, PVI.class, PVE.class }) Class type, + @Values(booleans = { true, false }) boolean single) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { var id = EXISTING_IDS.get(type); var instance = this.template.findById(id, type).orElseThrow(); @@ -75,11 +62,13 @@ class CascadingIT extends AbstractCascadingTestBase { if (single) { this.template.save(instance); - } else { + } + else { this.template.saveAll(List.of(instance, type.getDeclaredConstructor(String.class).newInstance("Parent2"))); } - // Can't assert on the instance above, as that would ofc be the purposefully modified state + // Can't assert on the instance above, as that would ofc be the purposefully + // modified state var reloadedInstance = this.template.findById(id, type).orElseThrow(); assertThat(reloadedInstance.getName()).isEqualTo("Updated parent"); @@ -87,22 +76,44 @@ class CascadingIT extends AbstractCascadingTestBase { assertThat(reloadedInstance.getSingleCUI().getName()).isEqualTo("ParentDB.singleCUI"); assertThat(reloadedInstance.getSingleCVE().getVersion()).isZero(); assertThat(reloadedInstance.getSingleCVI().getVersion()).isZero(); - assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2"))); + assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") + && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2"))); assertThat(reloadedInstance.getManyCVI()).allMatch(cvi -> cvi.getVersion() == 0L); } @CartesianTest void newItemsMustBePersistedRegardlessOfCascadeSingleSave( - @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type, - @Values(booleans = {true, false}) boolean single) throws Exception { + @Values(classes = { PUI.class, PUE.class, PVI.class, PVE.class }) Class type, + @Values(booleans = { true, false }) boolean single) throws Exception { T instance; if (single) { - instance = template.save(type.getDeclaredConstructor(String.class).newInstance("Parent")); - } else { - instance = template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), type.getDeclaredConstructor(String.class).newInstance("Parent2"))).get(0); + instance = this.template.save(type.getDeclaredConstructor(String.class).newInstance("Parent")); + } + else { + instance = this.template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), + type.getDeclaredConstructor(String.class).newInstance("Parent2"))) + .get(0); } assertAllRelationshipsHaveBeenCreated(instance); } + + @EnableTransactionManagement + @ComponentScan + static class Config extends Neo4jImperativeTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java index 263f2fe58..73ff120e0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java @@ -19,4 +19,5 @@ package org.springframework.data.neo4j.integration.cascading; * Marker for external id. */ public interface ExternalId { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java index 4980d9cd5..0b7267568 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java @@ -57,24 +57,20 @@ public class PUE implements Parent, ExternalId { this.name = name; this.singleCUI = new CUI(name + ".singleCUI"); this.singleCUE = new CUE(name + ".singleCUE"); - this.manyCUI = List.of( - new CUI(name + ".cUI1"), - new CUI(name + ".cUI2") - ); + this.manyCUI = List.of(new CUI(name + ".cUI1"), new CUI(name + ".cUI2")); this.singleCVI = new CVI(name + ".singleCVI"); this.singleCVE = new CVE(name + ".singleCVE"); - this.manyCVI = List.of( - new CVI(name + ".cVI1"), - new CVI(name + ".cVI2") - ); + this.manyCVI = List.of(new CVI(name + ".cVI1"), new CVI(name + ".cVI2")); } + @Override public String getId() { - return id; + return this.id; } + @Override public String getName() { - return name; + return this.name; } @Override @@ -84,31 +80,32 @@ public class PUE implements Parent, ExternalId { @Override public List getManyCUI() { - return manyCUI; + return this.manyCUI; } @Override public List getManyCVI() { - return manyCVI; + return this.manyCVI; } @Override public CUE getSingleCUE() { - return singleCUE; + return this.singleCUE; } @Override public CUI getSingleCUI() { - return singleCUI; + return this.singleCUI; } @Override public CVE getSingleCVE() { - return singleCVE; + return this.singleCVE; } @Override public CVI getSingleCVI() { - return singleCVI; + return this.singleCVI; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java index a0a48df80..22d4909d9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java @@ -56,25 +56,20 @@ public class PUI implements Parent { this.name = name; this.singleCUI = new CUI(name + ".singleCUI"); this.singleCUE = new CUE(name + ".singleCUE"); - this.manyCUI = List.of( - new CUI(name + ".cUI1"), - new CUI(name + ".cUI2") - ); + this.manyCUI = List.of(new CUI(name + ".cUI1"), new CUI(name + ".cUI2")); this.singleCVI = new CVI(name + ".singleCVI"); this.singleCVE = new CVE(name + ".singleCVE"); - this.manyCVI = List.of( - new CVI(name + ".cVI1"), - new CVI(name + ".cVI2") - ); + this.manyCVI = List.of(new CVI(name + ".cVI1"), new CVI(name + ".cVI2")); } + @Override public String getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override @@ -84,31 +79,32 @@ public class PUI implements Parent { @Override public List getManyCUI() { - return manyCUI; + return this.manyCUI; } @Override public List getManyCVI() { - return manyCVI; + return this.manyCVI; } @Override public CUE getSingleCUE() { - return singleCUE; + return this.singleCUE; } @Override public CUI getSingleCUI() { - return singleCUI; + return this.singleCUI; } @Override public CVE getSingleCVE() { - return singleCVE; + return this.singleCVE; } @Override public CVI getSingleCVI() { - return singleCVI; + return this.singleCVI; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java index 7958ffa0f..c42879af9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java @@ -61,25 +61,20 @@ public class PVE implements Parent, Versioned, ExternalId { this.name = name; this.singleCUI = new CUI(name + ".singleCUI"); this.singleCUE = new CUE(name + ".singleCUE"); - this.manyCUI = List.of( - new CUI(name + ".cUI1"), - new CUI(name + ".cUI2") - ); + this.manyCUI = List.of(new CUI(name + ".cUI1"), new CUI(name + ".cUI2")); this.singleCVI = new CVI(name + ".singleCVI"); this.singleCVE = new CVE(name + ".singleCVE"); - this.manyCVI = List.of( - new CVI(name + ".cVI1"), - new CVI(name + ".cVI2") - ); + this.manyCVI = List.of(new CVI(name + ".cVI1"), new CVI(name + ".cVI2")); } + @Override public String getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override @@ -89,36 +84,37 @@ public class PVE implements Parent, Versioned, ExternalId { @Override public Long getVersion() { - return version; + return this.version; } @Override public List getManyCUI() { - return manyCUI; + return this.manyCUI; } @Override public List getManyCVI() { - return manyCVI; + return this.manyCVI; } @Override public CUE getSingleCUE() { - return singleCUE; + return this.singleCUE; } @Override public CUI getSingleCUI() { - return singleCUI; + return this.singleCUI; } @Override public CVE getSingleCVE() { - return singleCVE; + return this.singleCVE; } @Override public CVI getSingleCVI() { - return singleCVI; + return this.singleCVI; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java index 244ab3fd6..54f9918c3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java @@ -60,25 +60,20 @@ public class PVI implements Parent, Versioned { this.name = name; this.singleCUI = new CUI(name + ".singleCUI"); this.singleCUE = new CUE(name + ".singleCUE"); - this.manyCUI = List.of( - new CUI(name + ".cUI1"), - new CUI(name + ".cUI2") - ); + this.manyCUI = List.of(new CUI(name + ".cUI1"), new CUI(name + ".cUI2")); this.singleCVI = new CVI(name + ".singleCVI"); this.singleCVE = new CVE(name + ".singleCVE"); - this.manyCVI = List.of( - new CVI(name + ".cVI1"), - new CVI(name + ".cVI2") - ); + this.manyCVI = List.of(new CVI(name + ".cVI1"), new CVI(name + ".cVI2")); } + @Override public String getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override @@ -88,36 +83,37 @@ public class PVI implements Parent, Versioned { @Override public Long getVersion() { - return version; + return this.version; } @Override public List getManyCUI() { - return manyCUI; + return this.manyCUI; } @Override public List getManyCVI() { - return manyCVI; + return this.manyCVI; } @Override public CUE getSingleCUE() { - return singleCUE; + return this.singleCUE; } @Override public CUI getSingleCUI() { - return singleCUI; + return this.singleCUI; } @Override public CVE getSingleCVE() { - return singleCVE; + return this.singleCVE; } @Override public CVI getSingleCVI() { - return singleCVI; + return this.singleCVI; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java index 7786a5b3c..5a8ba2652 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java @@ -39,4 +39,5 @@ public interface Parent { CVE getSingleCVE(); CVI getSingleCVI(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java index f806807bf..a36041672 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java @@ -15,14 +15,13 @@ */ package org.springframework.data.neo4j.integration.cascading; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.reflect.InvocationTargetException; import java.util.List; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.junitpioneer.jupiter.cartesian.CartesianTest.Values; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -33,34 +32,22 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + @Neo4jIntegrationTest @Import(ReactiveCascadingIT.Config.class) class ReactiveCascadingIT extends AbstractCascadingTestBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - @EnableTransactionManagement - @ComponentScan - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } - @Autowired ReactiveNeo4jTemplate template; @CartesianTest void updatesMustNotCascade( - @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type, - @Values(booleans = {true, false}) boolean single) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + @Values(classes = { PUI.class, PUE.class, PVI.class, PVE.class }) Class type, + @Values(booleans = { true, false }) boolean single) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { var id = EXISTING_IDS.get(type); var instance = this.template.findById(id, type).single().block(); @@ -75,11 +62,15 @@ class ReactiveCascadingIT extends AbstractCascadingTestBase { if (single) { this.template.save(instance).block(); - } else { - this.template.saveAll(List.of(instance, type.getDeclaredConstructor(String.class).newInstance("Parent2"))).collectList().block(); + } + else { + this.template.saveAll(List.of(instance, type.getDeclaredConstructor(String.class).newInstance("Parent2"))) + .collectList() + .block(); } - // Can't assert on the instance above, as that would ofc be the purposefully modified state + // Can't assert on the instance above, as that would ofc be the purposefully + // modified state var reloadedInstance = this.template.findById(id, type).singleOptional().block().orElseThrow(); assertThat(reloadedInstance.getName()).isEqualTo("Updated parent"); @@ -87,25 +78,46 @@ class ReactiveCascadingIT extends AbstractCascadingTestBase { assertThat(reloadedInstance.getSingleCUI().getName()).isEqualTo("ParentDB.singleCUI"); assertThat(reloadedInstance.getSingleCVE().getVersion()).isZero(); assertThat(reloadedInstance.getSingleCVI().getVersion()).isZero(); - assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2"))); + assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") + && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2"))); assertThat(reloadedInstance.getManyCVI()).allMatch(cvi -> cvi.getVersion() == 0L); } @CartesianTest void newItemsMustBePersistedRegardlessOfCascadeSingleSave( - @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type, - @Values(booleans = {true, false}) boolean single) throws Exception { + @Values(classes = { PUI.class, PUE.class, PVI.class, PVE.class }) Class type, + @Values(booleans = { true, false }) boolean single) throws Exception { T instance; if (single) { - instance = template.save(type.getDeclaredConstructor(String.class).newInstance("Parent")).block(); - } else { - instance = template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), type.getDeclaredConstructor(String.class).newInstance("Parent2"))) - .collectList() - .block() - .get(0); + instance = this.template.save(type.getDeclaredConstructor(String.class).newInstance("Parent")).block(); + } + else { + instance = this.template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), + type.getDeclaredConstructor(String.class).newInstance("Parent2"))) + .collectList() + .block() + .get(0); } assertAllRelationshipsHaveBeenCreated(instance); } + + @EnableTransactionManagement + @ComponentScan + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java index fe98bd234..690850868 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java @@ -21,4 +21,5 @@ package org.springframework.data.neo4j.integration.cascading; public interface Versioned { Long getVersion(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java index 46fdfd2f1..69d849260 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java @@ -16,12 +16,10 @@ package org.springframework.data.neo4j.integration.cdi; import jakarta.inject.Inject; - import org.neo4j.driver.Driver; /** * @author Michael J. Simons - * @soundtrack Various - TRON Legacy R3conf1gur3d */ class Neo4jBasedService { @@ -34,4 +32,5 @@ class Neo4jBasedService { this.driver = driver; this.personRepository = personRepository; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java index 20cc4c061..841ecde3a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.cdi; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.Optional; import jakarta.enterprise.context.ApplicationScoped; @@ -26,13 +23,13 @@ import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; import jakarta.inject.Singleton; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Dialect; import org.neo4j.driver.Driver; + import org.springframework.data.neo4j.config.Neo4jCdiExtension; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jOperations; @@ -40,94 +37,26 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Michael J. Simons - * @soundtrack Various - TRON Legacy R3conf1gur3d */ @ExtendWith(Neo4jExtension.class) class Neo4jCdiExtensionIT { protected static Neo4jExtension.Neo4jConnectionSupport connectionSupport; - @ApplicationScoped - static class RealDriverFactory { - - @Produces - @Singleton - public Driver driver() { - return connectionSupport.getDriver(); - } - - @Produces - @Singleton - public Configuration cypherDslConfiguration() { - if (connectionSupport.isCypher5SyntaxCompatible()) { - return Configuration.newConfig().withDialect(Dialect.NEO4J_5).build(); - } - - return Configuration.newConfig().withDialect(Dialect.NEO4J_4).build(); - } - } - - @ApplicationScoped - static class MockedDriverFactory { - - @Produces - @Singleton - public Driver driver() { - return Mockito.mock(Driver.class); - } - } - - @ApplicationScoped - static class CustomDependencyProducer { - - Neo4jConversions conversions = Mockito.mock(Neo4jConversions.class); - - DatabaseSelectionProvider databaseSelectionProvider = Mockito.mock(DatabaseSelectionProvider.class); - - Neo4jOperations neo4jOperations = Mockito.mock(Neo4jOperations.class); - - @Produces @Singleton - public Neo4jConversions getConversions() { - return conversions; - } - - @Produces @Singleton - public DatabaseSelectionProvider getDatabaseSelectionProvider() { - return databaseSelectionProvider; - } - - @Produces @Singleton - public Neo4jOperations getNeo4jOperations() { - return neo4jOperations; - } - } - - @ApplicationScoped - static class BrokenCustomDependencyProducer { - - @Produces @Singleton - public Neo4jConversions getConversions1() { - return Mockito.mock(Neo4jConversions.class); - } - - @Produces @Singleton - public Neo4jConversions getConversions2() { - return Mockito.mock(Neo4jConversions.class); - } - } - @Test void cdiExtensionShouldProduceFunctionalRepositories() { try (SeContainer container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addExtensions(Neo4jCdiExtension.class) - .addBeanClasses(RealDriverFactory.class, PersonRepository.class, Neo4jBasedService.class) - .initialize()) { - Neo4jBasedService client = container - .select(Neo4jBasedService.class).get(); + .disableDiscovery() + .addExtensions(Neo4jCdiExtension.class) + .addBeanClasses(RealDriverFactory.class, PersonRepository.class, Neo4jBasedService.class) + .initialize()) { + Neo4jBasedService client = container.select(Neo4jBasedService.class).get(); assertThat(client).isNotNull(); assertThat(client.driver).isNotNull(); @@ -146,22 +75,18 @@ class Neo4jCdiExtensionIT { Class configurationSupport = getNeo4jCdiConfigurationSupport(); try (SeContainer container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses( - MockedDriverFactory.class, - CustomDependencyProducer.class, - configurationSupport - ) - .initialize()) { + .disableDiscovery() + .addBeanClasses(MockedDriverFactory.class, CustomDependencyProducer.class, configurationSupport) + .initialize()) { CustomDependencyProducer customDependencyProducer = container.select(CustomDependencyProducer.class).get(); assertThat(container.select(Neo4jConversions.class).get()) - .isEqualTo(customDependencyProducer.getConversions()); + .isEqualTo(customDependencyProducer.getConversions()); assertThat(container.select(DatabaseSelectionProvider.class).get()) - .isEqualTo(customDependencyProducer.getDatabaseSelectionProvider()); + .isEqualTo(customDependencyProducer.getDatabaseSelectionProvider()); assertThat(container.select(Neo4jOperations.class).get()) - .isEqualTo(customDependencyProducer.getNeo4jOperations()); + .isEqualTo(customDependencyProducer.getNeo4jOperations()); } } @@ -170,13 +95,9 @@ class Neo4jCdiExtensionIT { Class configurationSupport = getNeo4jCdiConfigurationSupport(); try (SeContainer container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses( - MockedDriverFactory.class, - BrokenCustomDependencyProducer.class, - configurationSupport - ) - .initialize()) { + .disableDiscovery() + .addBeanClasses(MockedDriverFactory.class, BrokenCustomDependencyProducer.class, configurationSupport) + .initialize()) { assertThatExceptionOfType(AmbiguousResolutionException.class).isThrownBy(() -> { Neo4jMappingContext context = container.select(Neo4jMappingContext.class).get(); @@ -190,8 +111,88 @@ class Neo4jCdiExtensionIT { // Wrapped in a reflection call so that we don't need to make it public just // for testing it's producer methods. return Class.forName("org.springframework.data.neo4j.config.Neo4jCdiConfigurationSupport"); - } catch (ClassNotFoundException e) { - throw new RuntimeException("¯\\_(ツ)_/¯", e); + } + catch (ClassNotFoundException ex) { + throw new RuntimeException("¯\\_(ツ)_/¯", ex); } } + + @ApplicationScoped + public static class RealDriverFactory { + + @Produces + @Singleton + public Driver driver() { + return connectionSupport.getDriver(); + } + + @Produces + @Singleton + public Configuration cypherDslConfiguration() { + if (connectionSupport.isCypher5SyntaxCompatible()) { + return Configuration.newConfig().withDialect(Dialect.NEO4J_5).build(); + } + + return Configuration.newConfig().withDialect(Dialect.NEO4J_4).build(); + } + + } + + @ApplicationScoped + static class MockedDriverFactory { + + @Produces + @Singleton + Driver driver() { + return Mockito.mock(Driver.class); + } + + } + + @ApplicationScoped + public static class CustomDependencyProducer { + + Neo4jConversions conversions = Mockito.mock(Neo4jConversions.class); + + DatabaseSelectionProvider databaseSelectionProvider = Mockito.mock(DatabaseSelectionProvider.class); + + Neo4jOperations neo4jOperations = Mockito.mock(Neo4jOperations.class); + + @Produces + @Singleton + public Neo4jConversions getConversions() { + return this.conversions; + } + + @Produces + @Singleton + public DatabaseSelectionProvider getDatabaseSelectionProvider() { + return this.databaseSelectionProvider; + } + + @Produces + @Singleton + public Neo4jOperations getNeo4jOperations() { + return this.neo4jOperations; + } + + } + + @ApplicationScoped + public static class BrokenCustomDependencyProducer { + + @Produces + @Singleton + public Neo4jConversions getConversions1() { + return Mockito.mock(Neo4jConversions.class); + } + + @Produces + @Singleton + public Neo4jConversions getConversions2() { + return Mockito.mock(Neo4jConversions.class); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java index 49fb155b0..e6450a06d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java @@ -24,37 +24,37 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; /** - * This domain object features a client side generated ID on purpose. It is needed to verify that the callbacks generating - * those are actually registered correct. + * This domain object features a client side generated ID on purpose. It is needed to + * verify that the callbacks generating those are actually registered correct. * * @author Michael J. Simons - * @soundtrack Various - TRON Legacy R3conf1gur3d */ @Node class Person { - @Id @GeneratedValue + private final String name; + + @Id + @GeneratedValue private UUID id; @CreatedDate private LocalDate createdAt; - private final String name; - Person(String name) { this.name = name; } - public UUID getId() { - return id; + UUID getId() { + return this.id; } - public String getName() { - return name; + String getName() { + return this.name; } - public LocalDate getCreatedAt() { - return createdAt; + LocalDate getCreatedAt() { + return this.createdAt; } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java index 976831abd..c4c83f413 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java @@ -21,7 +21,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; /** * @author Michael J. Simons - * @soundtrack Various - TRON Legacy R3conf1gur3d */ interface PersonRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/CustomTypesIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/CustomTypesIT.java index ce7266c74..cd9c26704 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/CustomTypesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/CustomTypesIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -35,11 +33,11 @@ import org.neo4j.driver.Session; import org.neo4j.driver.TransactionCallback; import org.neo4j.driver.Values; import org.neo4j.driver.summary.ResultSummary; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.convert.Neo4jConversions; @@ -53,11 +51,14 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.repository.query.Param; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier */ @@ -83,63 +84,62 @@ public class CustomTypesIT { TransactionCallback createPersonWithCustomId(PersonWithCustomId.PersonId assignedId) { - return tx -> tx.run("CREATE (n:PersonWithCustomId) SET n.id = $id ", - Values.parameters("id", assignedId.getId())).consume(); + return tx -> tx + .run("CREATE (n:PersonWithCustomId) SET n.id = $id ", Values.parameters("id", assignedId.getId())) + .consume(); } @BeforeEach void setupData() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.executeWrite(transaction -> { transaction.run("MATCH (n) detach delete n").consume(); transaction.run("CREATE (:CustomTypes{customType:'XYZ', dateAsLong: 1630311077418})").consume(); transaction.run("CREATE (:CustomTypes{customType:'ABC'})").consume(); return null; }); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test void deleteByCustomId() { - PersonWithCustomId.PersonId id = new PersonWithCustomId.PersonId(customIdValueGenerator.incrementAndGet()); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + PersonWithCustomId.PersonId id = new PersonWithCustomId.PersonId(this.customIdValueGenerator.incrementAndGet()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.executeWrite(createPersonWithCustomId(id)); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - assertThat(neo4jOperations.count(PersonWithCustomId.class)).isEqualTo(1L); - neo4jOperations.deleteById(id, PersonWithCustomId.class); + assertThat(this.neo4jOperations.count(PersonWithCustomId.class)).isEqualTo(1L); + this.neo4jOperations.deleteById(id, PersonWithCustomId.class); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithCustomId) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test void deleteAllByCustomId() { - List ids = Stream.generate(customIdValueGenerator::incrementAndGet) - .map(PersonWithCustomId.PersonId::new) - .limit(2) - .collect(Collectors.toList()); - try ( - Session session = driver.session(bookmarkCapture.createSessionConfig()) - ) { + List ids = Stream.generate(this.customIdValueGenerator::incrementAndGet) + .map(PersonWithCustomId.PersonId::new) + .limit(2) + .collect(Collectors.toList()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { ids.forEach(id -> session.executeWrite(createPersonWithCustomId(id))); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - assertThat(neo4jOperations.count(PersonWithCustomId.class)).isEqualTo(2L); - neo4jOperations.deleteAllById(ids, PersonWithCustomId.class); + assertThat(this.neo4jOperations.count(PersonWithCustomId.class)).isEqualTo(2L); + this.neo4jOperations.deleteAllById(ids, PersonWithCustomId.class); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithCustomId) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -176,7 +176,8 @@ public class CustomTypesIT { @Test // GH-2365 void converterAndProjection(@Autowired EntityWithCustomTypePropertyRepository repository) { - ThingWithCustomTypesProjection projection = repository.converterOnProjection(ThingWithCustomTypes.CustomType.of("XYZ")); + ThingWithCustomTypesProjection projection = repository + .converterOnProjection(ThingWithCustomTypes.CustomType.of("XYZ")); assertThat(projection).isNotNull(); assertThat(projection.dateAsLong).isInSameDayAs("2021-08-30"); assertThat(projection.modified).isInSameDayAs("2021-09-21"); @@ -187,7 +188,7 @@ public class CustomTypesIT { @Autowired EntityWithCustomTypePropertyRepository repository) { assertThat(repository.findByCustomTypeCustomSpELPropertyAccessQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) - .isNotNull(); + .isNotNull(); } @Test @@ -202,26 +203,21 @@ public class CustomTypesIT { assertThat(repository.findByDifferentTypeCustomQuery(ThingWithCustomTypes.DifferentType.of("XYZ"))).isNotNull(); } - static class ThingWithCustomTypesProjection { - - Date dateAsLong; - - @DateLong - Date modified; - } - interface EntityWithCustomTypePropertyRepository extends Neo4jRepository { ThingWithCustomTypes findByCustomType(ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = $customType return c") - ThingWithCustomTypes findByCustomTypeCustomQuery(@Param("customType") ThingWithCustomTypes.CustomType customType); + ThingWithCustomTypes findByCustomTypeCustomQuery( + @Param("customType") ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = $customType return c, 1632200400000 as modified") - ThingWithCustomTypesProjection converterOnProjection(@Param("customType") ThingWithCustomTypes.CustomType customType); + ThingWithCustomTypesProjection converterOnProjection( + @Param("customType") ThingWithCustomTypes.CustomType customType); @Query("MATCH (thingWithCustomTypes:`CustomTypes`) WHERE thingWithCustomTypes.customType = $0 RETURN thingWithCustomTypes{.customType, dateAsLong: COALESCE(thingWithCustomTypes.dateAsLong, 1632200400000), .dateAsString, .id, __nodeLabels__: labels(thingWithCustomTypes), __internalNeo4jId__: id(thingWithCustomTypes)}") - ThingWithCustomTypes defaultAttributeWithCoalesce(@Param("customType") ThingWithCustomTypes.CustomType customType); + ThingWithCustomTypes defaultAttributeWithCoalesce( + @Param("customType") ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = $differentType return c") ThingWithCustomTypes findByDifferentTypeCustomQuery( @@ -234,6 +230,16 @@ public class CustomTypesIT { @Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType} return c") ThingWithCustomTypes findByCustomTypeSpELObjectQuery( @Param("customType") ThingWithCustomTypes.CustomType customType); + + } + + static class ThingWithCustomTypesProjection { + + Date dateAsLong; + + @DateLong + Date modified; + } @Configuration @@ -258,26 +264,31 @@ public class CustomTypesIT { return new Neo4jConversions(additionalConverters); } - @Override // needed here because there is no implicit registration of entities upfront some methods under test + @Override // needed here because there is no implicit registration of entities + // upfront some methods under test protected Collection getMappingBasePackages() { return Collections.singletonList(ThingWithCustomTypes.class.getPackage().getName()); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java index acf744dfd..381c4e4a0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import org.junit.jupiter.api.Test; @@ -24,6 +22,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.types.Node; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -43,14 +42,16 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Learning English, Lesson Two */ @Neo4jIntegrationTest class ImperativeCompositePropertiesIT extends CompositePropertiesITBase { - @Autowired ImperativeCompositePropertiesIT(Driver driver, BookmarkCapture bookmarkCapture) { + @Autowired + ImperativeCompositePropertiesIT(Driver driver, BookmarkCapture bookmarkCapture) { super(driver, bookmarkCapture); } @@ -92,25 +93,19 @@ class ImperativeCompositePropertiesIT extends CompositePropertiesITBase { thing.setSomeOtherDTO(null); repository.save(thing); - try (Session session = driver.session()) { - Record r = session.executeRead(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", - Collections.singletonMap("id", id)).single()); + try (Session session = this.driver.session()) { + Record r = session.executeRead(tx -> tx + .run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", Collections.singletonMap("id", id)) + .single()); Node n = r.get("t").asNode(); - assertThat(n.asMap()).doesNotContainKeys( - "someDatesByEnumA.VALUE_AA", - "datesWithTransformedKey.test", - "dto.x", "dto.y", "dto.z" - ); + assertThat(n.asMap()).doesNotContainKeys("someDatesByEnumA.VALUE_AA", "datesWithTransformedKey.test", + "dto.x", "dto.y", "dto.z"); } } - public interface ThingProjection { - - ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO(); - } - @Test // GH-2451 - void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, @Autowired Neo4jTemplate template) { + void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, + @Autowired Neo4jTemplate template) { Long id = createNodeWithCompositeProperties(); ThingWithCompositeProperties thing = repository.findById(id).get(); @@ -119,20 +114,24 @@ class ImperativeCompositePropertiesIT extends CompositePropertiesITBase { thing.setSomeOtherDTO(null); template.saveAs(thing, ThingProjection.class); - try (Session session = driver.session()) { - Record r = session.executeRead(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", - Collections.singletonMap("id", id)).single()); + try (Session session = this.driver.session()) { + Record r = session.executeRead(tx -> tx + .run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", Collections.singletonMap("id", id)) + .single()); Node n = r.get("t").asNode(); - assertThat(n.asMap()) - .containsKeys( - "someDatesByEnumA.VALUE_AA", - "datesWithTransformedKey.test" - ) - .doesNotContainKeys("dto.x", "dto.y", "dto.z"); + assertThat(n.asMap()).containsKeys("someDatesByEnumA.VALUE_AA", "datesWithTransformedKey.test") + .doesNotContainKeys("dto.x", "dto.y", "dto.z"); } } + public interface ThingProjection { + + ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO(); + + } + public interface Repository extends Neo4jRepository { + } @Configuration @@ -141,6 +140,7 @@ class ImperativeCompositePropertiesIT extends CompositePropertiesITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -151,20 +151,24 @@ class ImperativeCompositePropertiesIT extends CompositePropertiesITBase { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/Neo4jConversionsIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/Neo4jConversionsIT.java index 89bb2a331..d8873d4a1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/Neo4jConversionsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/Neo4jConversionsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; @@ -37,6 +35,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Session; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.ConverterBuilder; @@ -44,6 +43,8 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.integration.shared.conversion.Neo4jConversionsITBase; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -51,6 +52,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; class Neo4jConversionsIT extends Neo4jConversionsITBase { private static final TypeDescriptor TYPE_DESCRIPTOR_OF_VALUE = TypeDescriptor.valueOf(Value.class); + private static final DefaultConversionService DEFAULT_CONVERSION_SERVICE = new DefaultConversionService(); @BeforeAll @@ -58,6 +60,59 @@ class Neo4jConversionsIT extends Neo4jConversionsITBase { new Neo4jConversions().registerConvertersIn(DEFAULT_CONVERSION_SERVICE); } + static void assertRead(String label, String attribute, Object t) { + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { + Value v = session + .run("MATCH (n) WHERE labels(n) = [$label] RETURN n[$attribute] as r", + Values.parameters("label", label, "attribute", attribute)) + .single() + .get("r"); + + TypeDescriptor typeDescriptor = TypeDescriptor.forObject(t); + if (typeDescriptor.isCollection()) { + Collection collection = (Collection) t; + Class targetType = collection.stream().map(Object::getClass).findFirst().get(); + List convertedObjects = v.asList(elem -> DEFAULT_CONVERSION_SERVICE.convert(elem, targetType)); + assertThat(convertedObjects).containsAll(collection); + } + else { + Object converted = DEFAULT_CONVERSION_SERVICE.convert(v, typeDescriptor.getType()); + assertThat(converted).isEqualTo(t); + } + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + + static void assertWrite(String label, String attribute, Object t) { + + Value driverValue; + if (t != null && Collection.class.isAssignableFrom(t.getClass())) { + Collection sourceCollection = (Collection) t; + Object[] targetCollection = (sourceCollection).stream() + .map(element -> DEFAULT_CONVERSION_SERVICE.convert(element, Value.class)) + .toArray(); + driverValue = Values.value(targetCollection); + } + else { + driverValue = DEFAULT_CONVERSION_SERVICE.convert(t, Value.class); + } + + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { + Map parameters = new HashMap<>(); + parameters.put("label", label); + parameters.put("attribute", attribute); + parameters.put("v", driverValue); + + long cnt = session + .run("MATCH (n) WHERE labels(n) = [$label] AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) + .single() + .get("cnt") + .asLong(); + assertThat(cnt).isEqualTo(1L); + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + @TestFactory @DisplayName("Objects") Stream objects() { @@ -68,13 +123,19 @@ class Neo4jConversionsIT extends Neo4jConversionsITBase { return supportedTypes.entrySet().stream().map(types -> { - DynamicContainer reads = DynamicContainer.dynamicContainer("read", types.getValue().entrySet().stream().map( - a -> DynamicTest - .dynamicTest(a.getKey(), () -> Neo4jConversionsIT.assertRead(types.getKey(), a.getKey(), a.getValue())))); + DynamicContainer reads = DynamicContainer.dynamicContainer("read", + types.getValue() + .entrySet() + .stream() + .map(a -> DynamicTest.dynamicTest(a.getKey(), + () -> Neo4jConversionsIT.assertRead(types.getKey(), a.getKey(), a.getValue())))); DynamicContainer writes = DynamicContainer.dynamicContainer("write", - types.getValue().entrySet().stream().map(a -> DynamicTest.dynamicTest(a.getKey(), - () -> Neo4jConversionsIT.assertWrite(types.getKey(), a.getKey(), a.getValue())))); + types.getValue() + .entrySet() + .stream() + .map(a -> DynamicTest.dynamicTest(a.getKey(), + () -> Neo4jConversionsIT.assertWrite(types.getKey(), a.getKey(), a.getValue())))); return DynamicContainer.dynamicContainer(types.getKey(), Arrays.asList(reads, writes)); }); @@ -96,9 +157,11 @@ class Neo4jConversionsIT extends Neo4jConversionsITBase { }).andWriting(d -> { if (d.isBefore(LocalDate.now())) { return Values.value("gestern"); - } else if (d.isAfter(LocalDate.now())) { + } + else if (d.isAfter(LocalDate.now())) { return Values.value("morgen"); - } else { + } + else { return Values.value("heute"); } }); @@ -107,10 +170,11 @@ class Neo4jConversionsIT extends Neo4jConversionsITBase { return Stream.of( DynamicTest.dynamicTest("read", () -> assertThat(customConversionService.convert(Values.value("gestern"), LocalDate.class)) - .isEqualTo(LocalDate.now().minusDays(1))), + .isEqualTo(LocalDate.now().minusDays(1))), DynamicTest.dynamicTest("write", - () -> assertThat(customConversionService.convert(LocalDate.now().plusDays(1), TYPE_DESCRIPTOR_OF_VALUE)) - .isEqualTo(Values.value("morgen")))); + () -> assertThat( + customConversionService.convert(LocalDate.now().plusDays(1), TYPE_DESCRIPTOR_OF_VALUE)) + .isEqualTo(Values.value("morgen")))); } @Nested @@ -146,50 +210,7 @@ class Neo4jConversionsIT extends Neo4jConversionsITBase { short s = DEFAULT_CONVERSION_SERVICE.convert(Values.value((short) 127), short.class); assertThat(s).isEqualTo((short) 127); } + } - static void assertRead(String label, String attribute, Object t) { - try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - Value v = session.run("MATCH (n) WHERE labels(n) = [$label] RETURN n[$attribute] as r", - Values.parameters("label", label, "attribute", attribute)).single().get("r"); - - TypeDescriptor typeDescriptor = TypeDescriptor.forObject(t); - if (typeDescriptor.isCollection()) { - Collection collection = (Collection) t; - Class targetType = collection.stream().map(Object::getClass).findFirst().get(); - List convertedObjects = v.asList(elem -> DEFAULT_CONVERSION_SERVICE.convert(elem, targetType)); - assertThat(convertedObjects).containsAll(collection); - } else { - Object converted = DEFAULT_CONVERSION_SERVICE.convert(v, typeDescriptor.getType()); - assertThat(converted).isEqualTo(t); - } - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - - static void assertWrite(String label, String attribute, Object t) { - - Value driverValue; - if (t != null && Collection.class.isAssignableFrom(t.getClass())) { - Collection sourceCollection = (Collection) t; - Object[] targetCollection = (sourceCollection).stream() - .map(element -> DEFAULT_CONVERSION_SERVICE.convert(element, Value.class)).toArray(); - driverValue = Values.value(targetCollection); - } else { - driverValue = DEFAULT_CONVERSION_SERVICE.convert(t, Value.class); - } - - try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - Map parameters = new HashMap<>(); - parameters.put("label", label); - parameters.put("attribute", attribute); - parameters.put("v", driverValue); - - long cnt = session - .run("MATCH (n) WHERE labels(n) = [$label] AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) - .single().get("cnt").asLong(); - assertThat(cnt).isEqualTo(1L); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java index bdf6d543b..86211fb7a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.net.URL; import java.text.SimpleDateFormat; import java.time.ZoneId; @@ -45,6 +42,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -76,10 +74,12 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Michael J. Simons * @author Dennis Crissman - * @soundtrack Tool - Fear Inoculum */ @Neo4jIntegrationTest class TypeConversionIT extends Neo4jConversionsITBase { @@ -94,16 +94,16 @@ class TypeConversionIT extends Neo4jConversionsITBase { private final DefaultConversionService defaultConversionService; - @Autowired TypeConversionIT(CypherTypesRepository cypherTypesRepository, - AdditionalTypesRepository additionalTypesRepository, SpatialTypesRepository spatialTypesRepository, - CustomTypesRepository customTypesRepository, + @Autowired + TypeConversionIT(CypherTypesRepository cypherTypesRepository, AdditionalTypesRepository additionalTypesRepository, + SpatialTypesRepository spatialTypesRepository, CustomTypesRepository customTypesRepository, Neo4jConversions neo4jConversions) { this.cypherTypesRepository = cypherTypesRepository; this.additionalTypesRepository = additionalTypesRepository; this.spatialTypesRepository = spatialTypesRepository; this.customTypesRepository = customTypesRepository; this.defaultConversionService = new DefaultConversionService(); - neo4jConversions.registerConvertersIn(defaultConversionService); + neo4jConversions.registerConvertersIn(this.defaultConversionService); } @Test @@ -112,15 +112,16 @@ class TypeConversionIT extends Neo4jConversionsITBase { Long id; try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - id = session.executeWrite(tx -> tx.run("CREATE (n:AllArgsCtorNoBuilder) RETURN id(n)").single().get(0).asLong()); + id = session + .executeWrite(tx -> tx.run("CREATE (n:AllArgsCtorNoBuilder) RETURN id(n)").single().get(0).asLong()); bookmarkCapture.seedWith(session.lastBookmarks()); } assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> template.findById(id, AllArgsCtorNoBuilder.class)) - .withMessageMatching("Error mapping Record<\\{.+: .*>") - .withRootCauseInstanceOf(IllegalArgumentException.class) - .withStackTraceContaining("Parameter aBoolean must not be null"); + .isThrownBy(() -> template.findById(id, AllArgsCtorNoBuilder.class)) + .withMessageMatching("Error mapping Record<\\{.+: .*>") + .withRootCauseInstanceOf(IllegalArgumentException.class) + .withStackTraceContaining("Parameter aBoolean must not be null"); } @TestFactory @@ -138,47 +139,51 @@ class TypeConversionIT extends Neo4jConversionsITBase { Object copyOfThing; switch (entry.getKey()) { case "CypherTypes" -> { - ThingWithAllCypherTypes hlp = cypherTypesRepository.findById(ID_OF_CYPHER_TYPES_NODE).get(); - copyOfThing = cypherTypesRepository.save(hlp.withId(null)); + ThingWithAllCypherTypes hlp = this.cypherTypesRepository.findById(ID_OF_CYPHER_TYPES_NODE).get(); + copyOfThing = this.cypherTypesRepository.save(hlp.withId(null)); thing = hlp; } case "AdditionalTypes" -> { - ThingWithAllAdditionalTypes hlp2 = additionalTypesRepository.findById(ID_OF_ADDITIONAL_TYPES_NODE) - .get(); - copyOfThing = additionalTypesRepository.save(hlp2.withId(null)); + ThingWithAllAdditionalTypes hlp2 = this.additionalTypesRepository + .findById(ID_OF_ADDITIONAL_TYPES_NODE) + .get(); + copyOfThing = this.additionalTypesRepository.save(hlp2.withId(null)); thing = hlp2; } case "SpatialTypes" -> { - ThingWithAllSpatialTypes hlp3 = spatialTypesRepository.findById(ID_OF_SPATIAL_TYPES_NODE).get(); - copyOfThing = spatialTypesRepository.save(hlp3.withId(null)); + ThingWithAllSpatialTypes hlp3 = this.spatialTypesRepository.findById(ID_OF_SPATIAL_TYPES_NODE) + .get(); + copyOfThing = this.spatialTypesRepository.save(hlp3.withId(null)); thing = hlp3; } case "CustomTypes" -> { - ThingWithCustomTypes hlp4 = customTypesRepository.findById(ID_OF_CUSTOM_TYPE_NODE).get(); - copyOfThing = customTypesRepository.save(hlp4.withId(null)); + ThingWithCustomTypes hlp4 = this.customTypesRepository.findById(ID_OF_CUSTOM_TYPE_NODE).get(); + copyOfThing = this.customTypesRepository.save(hlp4.withId(null)); thing = hlp4; } default -> throw new UnsupportedOperationException("Unsupported types: " + entry.getKey()); } DynamicContainer reads = DynamicContainer.dynamicContainer("read", - entry.getValue().entrySet().stream().map(a -> DynamicTest.dynamicTest(a.getKey(), - () -> { - Object actual = ReflectionTestUtils.getField(thing, a.getKey()); - Object expected = a.getValue(); - if (actual instanceof URL && expected instanceof URL) { - // The host has been chosen to avoid interaction with the URLStreamHandler - // Should be enough for our comparision. - actual = ((URL) actual).getHost(); - expected = ((URL) expected).getHost(); - } - assertThat(actual).isEqualTo(expected); - }))); + entry.getValue().entrySet().stream().map(a -> DynamicTest.dynamicTest(a.getKey(), () -> { + Object actual = ReflectionTestUtils.getField(thing, a.getKey()); + Object expected = a.getValue(); + if (actual instanceof URL && expected instanceof URL) { + // The host has been chosen to avoid interaction with the + // URLStreamHandler + // Should be enough for our comparision. + actual = ((URL) actual).getHost(); + expected = ((URL) expected).getHost(); + } + assertThat(actual).isEqualTo(expected); + }))); - DynamicContainer writes = DynamicContainer.dynamicContainer("write", entry.getValue().keySet().stream() - .map(o -> DynamicTest - .dynamicTest(o, - () -> assertWrite(copyOfThing, o, defaultConversionService)))); + DynamicContainer writes = DynamicContainer.dynamicContainer("write", + entry.getValue() + .keySet() + .stream() + .map(o -> DynamicTest.dynamicTest(o, + () -> assertWrite(copyOfThing, o, this.defaultConversionService)))); return DynamicContainer.dynamicContainer(entry.getKey(), Arrays.asList(reads, writes)); }); @@ -192,9 +197,11 @@ class TypeConversionIT extends Neo4jConversionsITBase { Function conversion; if (fieldName.equals("dateAsLong")) { conversion = o -> Values.value(((Date) o).getTime()); - } else if (fieldName.equals("dateAsString")) { + } + else if (fieldName.equals("dateAsString")) { conversion = o -> Values.value(new SimpleDateFormat("yyyy-MM-dd").format(o)); - } else { + } + else { conversion = o -> conversionService.convert(o, Value.class); } Value driverValue; @@ -202,7 +209,8 @@ class TypeConversionIT extends Neo4jConversionsITBase { Collection sourceCollection = (Collection) domainValue; Object[] targetCollection = (sourceCollection).stream().map(conversion).toArray(); driverValue = Values.value(targetCollection); - } else { + } + else { driverValue = conversion.apply(domainValue); } @@ -220,23 +228,23 @@ class TypeConversionIT extends Neo4jConversionsITBase { parameters.put("v2_lower", doubleList.get(1) - 0.000001d); parameters.put("v1_upper", doubleList.get(0) + 0.000001d); parameters.put("v2_upper", doubleList.get(1) + 0.000001d); - cnt = session - .run(""" + cnt = session.run(""" MATCH (n) WHERE id(n) = $id AND n[$attribute][0] > $v1_lower AND n[$attribute][1] > $v2_lower AND n[$attribute][0] < $v1_upper AND n[$attribute][1] < $v2_upper RETURN COUNT(n) AS cnt - """, - parameters) - .single().get("cnt").asLong(); - } else { + """, parameters).single().get("cnt").asLong(); + } + else { parameters.put("v", driverValue); cnt = session - .run("MATCH (n) WHERE id(n) = $id AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) - .single().get("cnt").asLong(); + .run("MATCH (n) WHERE id(n) = $id AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) + .single() + .get("cnt") + .asLong(); } assertThat(cnt).isEqualTo(1L); } @@ -268,9 +276,11 @@ class TypeConversionIT extends Neo4jConversionsITBase { @Test void parametersTargetingConvertedAttributesMustBeConverted(@Autowired CustomTypesRepository repository) { - assertThat(repository.findAllByDateAsString(Date.from(ZonedDateTime.of(2013, 5, 6, - 12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant().truncatedTo(ChronoUnit.DAYS)))) - .hasSizeGreaterThan(0); + assertThat(repository + .findAllByDateAsString(Date.from(ZonedDateTime.of(2013, 5, 6, 12, 0, 0, 0, ZoneId.of("Europe/Berlin")) + .toInstant() + .truncatedTo(ChronoUnit.DAYS)))) + .hasSizeGreaterThan(0); } @Test // GH-2348 @@ -278,7 +288,8 @@ class TypeConversionIT extends Neo4jConversionsITBase { Long id; try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - id = session.executeWrite(tx -> tx.run("CREATE (n:ThingWithAllCypherTypes2) RETURN id(n)").single().get(0).asLong()); + id = session.executeWrite( + tx -> tx.run("CREATE (n:ThingWithAllCypherTypes2) RETURN id(n)").single().get(0).asLong()); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -306,25 +317,31 @@ class TypeConversionIT extends Neo4jConversionsITBase { void clientShouldUseCustomType(@Autowired Neo4jClient client) { Optional value = client.query("RETURN 'whatever'") - .fetchAs(ThingWithCustomTypes.CustomType.class).first(); + .fetchAs(ThingWithCustomTypes.CustomType.class) + .first(); assertThat(value).map(ThingWithCustomTypes.CustomType::getValue).hasValue("whatever"); } public interface ConvertedIDsRepository extends Neo4jRepository { + } public interface CypherTypesRepository extends Neo4jRepository { + } public interface AdditionalTypesRepository extends Neo4jRepository { + } public interface SpatialTypesRepository extends Neo4jRepository { + } public interface CustomTypesRepository extends Neo4jRepository { List findAllByDateAsString(Date theDate); + } @Configuration @@ -336,6 +353,7 @@ class TypeConversionIT extends Neo4jConversionsITBase { private ObjectProvider userSelectionProviders; @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -349,27 +367,31 @@ class TypeConversionIT extends Neo4jConversionsITBase { public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { return Neo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(userSelectionProviders.getIfUnique()) - .withNeo4jConversions(neo4jConversions()) - .build(); + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) + .withNeo4jConversions(neo4jConversions()) + .build(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return Neo4jConversionsITBase.bookmarkCapture; } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java index 965cf6ada..5fb2f738d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_imperative.compose_as_ids; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,6 +43,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -52,19 +53,8 @@ class CompositeIdsIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - interface ThingWithCompositePropertyRepository extends Neo4jRepository { - - Optional findByCompositeValue(CompositeValue compositeValue); - - List findAllByCompositeValueNot(CompositeValue compositeValue); - } - - interface ThingWithCompositeIdRepository extends Neo4jRepository { - } - - @BeforeEach - public void prepareDatabase(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { + void prepareDatabase(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { try (Session session = driver.session()) { session.run("MATCH (n:ThingWithCompositeProperty) DETACH DELETE n").consume(); @@ -88,16 +78,16 @@ class CompositeIdsIT { Optional reloaded = repository.findByCompositeValue(saved.getCompositeValue()); assertThat(reloaded).hasValueSatisfying(v -> assertThat(v.getName()).isEqualTo("foobar")); - assertThat(repository.findAllByCompositeValueNot(saved.getCompositeValue())) - .hasSize(1) - .element(0) - .satisfies(v -> assertThat(v.getCompositeValue()).isEqualTo(new CompositeValue("b", 1))); + assertThat(repository.findAllByCompositeValueNot(saved.getCompositeValue())).hasSize(1) + .element(0) + .satisfies(v -> assertThat(v.getCompositeValue()).isEqualTo(new CompositeValue("b", 1))); } @Test void compositeIdsShouldWork(@Autowired ThingWithCompositeIdRepository repository) { - ThingWithCompositeId saved = repository.save(new ThingWithCompositeId(new CompositeValue("a,", 1), "first entity")); + ThingWithCompositeId saved = repository + .save(new ThingWithCompositeId(new CompositeValue("a,", 1), "first entity")); assertThat(saved.getVersion()).isGreaterThanOrEqualTo(0); saved.setName("foobar"); @@ -113,8 +103,8 @@ class CompositeIdsIT { void findAllByCompositeIdsShouldWork(@Autowired ThingWithCompositeIdRepository repository) { int cnt = 0; - String[] value1Values = {"a", "b"}; - int[] value2Values = {1, 2}; + String[] value1Values = { "a", "b" }; + int[] value2Values = { 1, 2 }; List ids = new ArrayList<>(value1Values.length * value2Values.length); for (String value1 : value1Values) { for (int value2 : value2Values) { @@ -128,20 +118,32 @@ class CompositeIdsIT { List loadedThings = repository.findAllById(ids); Collections.sort(loadedThings, Comparator.comparing(ThingWithCompositeId::getName)); - assertThat(loadedThings) - .hasSize(ids.size()) - .satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 1"), Index.atIndex(0)) - .satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 3"), Index.atIndex(2)); + assertThat(loadedThings).hasSize(ids.size()) + .satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 1"), Index.atIndex(0)) + .satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 3"), Index.atIndex(2)); assertThat(repository.existsById(removedId)).isTrue(); } + interface ThingWithCompositePropertyRepository extends Neo4jRepository { + + Optional findByCompositeValue(CompositeValue compositeValue); + + List findAllByCompositeValueNot(CompositeValue compositeValue); + + } + + interface ThingWithCompositeIdRepository extends Neo4jRepository { + + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -152,20 +154,24 @@ class CompositeIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java index c4bc56daa..6dd2c7d73 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java @@ -20,6 +20,7 @@ import java.util.Map; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; @@ -39,7 +40,8 @@ public record CompositeValue(String value1, Integer value2) { if (property == null) { decomposed.put("value1", Values.NULL); decomposed.put("value2", Values.NULL); - } else { + } + else { decomposed.put("value1", Values.value(property.value1)); decomposed.put("value2", Values.value(property.value2)); } @@ -48,9 +50,9 @@ public record CompositeValue(String value1, Integer value2) { @Override public CompositeValue compose(Map source, Neo4jConversionService conversionService) { - return source.isEmpty() ? - null : - new CompositeValue(source.get("value1").asString(), source.get("value2").asInt()); + return source.isEmpty() ? null + : new CompositeValue(source.get("value1").asString(), source.get("value2").asInt()); } + } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java index 4290bd761..0b8b180b1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java @@ -41,11 +41,11 @@ public class ThingWithCompositeId { } public CompositeValue getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -53,6 +53,7 @@ public class ThingWithCompositeId { } public Long getVersion() { - return version; + return this.version; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java index c60be60db..6e4b1ad32 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java @@ -26,12 +26,13 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class ThingWithCompositeProperty { - @Id @GeneratedValue - private Long id; - @CompositeProperty(converter = CompositeValue.Converter.class) private final CompositeValue compositeValue; + @Id + @GeneratedValue + private Long id; + private String name; public ThingWithCompositeProperty(CompositeValue compositeValue, String name) { @@ -40,11 +41,11 @@ public class ThingWithCompositeProperty { } public CompositeValue getCompositeValue() { - return compositeValue; + return this.compositeValue; } public String getName() { - return name; + return this.name; } public void setName(String name) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCompositePropertiesIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCompositePropertiesIT.java index 86910b1eb..9d3d7ac79 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCompositePropertiesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCompositePropertiesIT.java @@ -15,20 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.neo4j.driver.Record; -import org.neo4j.driver.Session; -import org.neo4j.driver.types.Node; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; -import org.springframework.data.neo4j.core.convert.Neo4jConversions; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,22 +22,35 @@ import java.util.List; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.types.Node; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.conversion.CompositePropertiesITBase; import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCompositeProperties; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Learning English, Lesson Two */ @Neo4jIntegrationTest @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) @@ -67,9 +66,10 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { List recorded = new ArrayList<>(); repository.save(newEntityWithRelationshipWithCompositeProperties()) - .as(StepVerifier::create) - .recordWith(() -> recorded).expectNextCount(1L) - .verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> recorded) + .expectNextCount(1L) + .verifyComplete(); assertThat(recorded).hasSize(1); assertRelationshipPropertiesInGraph(recorded.get(0).getId()); @@ -80,9 +80,9 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { Long id = createRelationshipWithCompositeProperties(); repository.findById(id) - .as(StepVerifier::create) - .consumeNextWith(this::assertRelationshipPropertiesOn) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(this::assertRelationshipPropertiesOn) + .verifyComplete(); } @Test @@ -90,9 +90,10 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { List recorded = new ArrayList<>(); repository.save(newEntityWithCompositeProperties()) - .as(StepVerifier::create).recordWith(() -> recorded) - .expectNextCount(1L) - .verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> recorded) + .expectNextCount(1L) + .verifyComplete(); assertThat(recorded).hasSize(1); assertNodePropertiesInGraph(recorded.get(0).getId()); @@ -102,47 +103,43 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { void compositePropertiesOnNodesShouldBeRead(@Autowired Repository repository) { Long id = createNodeWithCompositeProperties(); - repository.findById(id) - .as(StepVerifier::create) - .consumeNextWith(this::assertNodePropertiesOn) - .verifyComplete(); + repository.findById(id).as(StepVerifier::create).consumeNextWith(this::assertNodePropertiesOn).verifyComplete(); + } + + @Test // GH-2451 + void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, + @Autowired ReactiveNeo4jTemplate template) { + + Long id = createNodeWithCompositeProperties(); + repository.findById(id).map(thing -> { + thing.setDatesWithTransformedKey(Collections.singletonMap("Test", null)); + thing.setSomeDatesByEnumA(Collections.singletonMap(ThingWithCompositeProperties.EnumA.VALUE_AA, null)); + thing.setSomeOtherDTO(null); + return thing; + }) + .flatMap(thing -> template.saveAs(thing, ThingProjection.class)) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + try (Session session = this.driver.session()) { + Record r = session.executeRead(tx -> tx + .run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", Collections.singletonMap("id", id)) + .single()); + Node n = r.get("t").asNode(); + assertThat(n.asMap()).containsKeys("someDatesByEnumA.VALUE_AA", "datesWithTransformedKey.test") + .doesNotContainKeys("dto.x", "dto.y", "dto.z"); + } } public interface ThingProjection { ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO(); - } - @Test // GH-2451 - void compositePropertiesShouldBeFilterableEvenOnNonMapTypes(@Autowired Repository repository, @Autowired ReactiveNeo4jTemplate template) { - - Long id = createNodeWithCompositeProperties(); - repository.findById(id) - .map(thing -> { - thing.setDatesWithTransformedKey(Collections.singletonMap("Test", null)); - thing.setSomeDatesByEnumA(Collections.singletonMap(ThingWithCompositeProperties.EnumA.VALUE_AA, null)); - thing.setSomeOtherDTO(null); - return thing; - }) - .flatMap(thing -> template.saveAs(thing, ThingProjection.class)) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); - - try (Session session = driver.session()) { - Record r = session.executeRead(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", - Collections.singletonMap("id", id)).single()); - Node n = r.get("t").asNode(); - assertThat(n.asMap()) - .containsKeys( - "someDatesByEnumA.VALUE_AA", - "datesWithTransformedKey.test" - ) - .doesNotContainKeys("dto.x", "dto.y", "dto.z"); - } } public interface Repository extends ReactiveNeo4jRepository { + } @Configuration @@ -151,6 +148,7 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -161,20 +159,24 @@ class ReactiveCompositePropertiesIT extends CompositePropertiesITBase { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java index ac1edf6ef..e7d09f3a5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java @@ -15,19 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.neo4j.driver.TransactionCallback; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -43,31 +30,44 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Result; import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionCallback; import org.neo4j.driver.Values; import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.conversion.PersonWithCustomId; import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.repository.query.Param; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier * @author Michael J. Simons */ @Neo4jIntegrationTest @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) -@TestPropertySource(properties = {"foo=XYZ"}) +@TestPropertySource(properties = { "foo=XYZ" }) public class ReactiveCustomTypesIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; @@ -77,10 +77,12 @@ public class ReactiveCustomTypesIT { private final Driver driver; private final ReactiveNeo4jOperations neo4jOperations; + private final BookmarkCapture bookmarkCapture; @Autowired - public ReactiveCustomTypesIT(Driver driver, ReactiveNeo4jOperations neo4jOperations, BookmarkCapture bookmarkCapture) { + public ReactiveCustomTypesIT(Driver driver, ReactiveNeo4jOperations neo4jOperations, + BookmarkCapture bookmarkCapture) { this.driver = driver; this.neo4jOperations = neo4jOperations; this.bookmarkCapture = bookmarkCapture; @@ -88,26 +90,28 @@ public class ReactiveCustomTypesIT { @BeforeEach void setup() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.executeWrite(transaction -> { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (:CustomTypes{customType:'XYZ'})"); return null; }); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test void findByConvertedCustomType(@Autowired EntityWithCustomTypePropertyRepository repository) { - StepVerifier.create(repository.findByCustomType(ThingWithCustomTypes.CustomType.of("XYZ"))).expectNextCount(1) - .verifyComplete(); + StepVerifier.create(repository.findByCustomType(ThingWithCustomTypes.CustomType.of("XYZ"))) + .expectNextCount(1) + .verifyComplete(); } @Test void findByConvertedCustomTypeWithCustomQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { StepVerifier.create(repository.findByCustomTypeCustomQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) - .expectNextCount(1).verifyComplete(); + .expectNextCount(1) + .verifyComplete(); } @Test @@ -115,69 +119,74 @@ public class ReactiveCustomTypesIT { @Autowired EntityWithCustomTypePropertyRepository repository) { StepVerifier - .create(repository.findByCustomTypeCustomSpELPropertyAccessQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) - .expectNextCount(1).verifyComplete(); + .create(repository.findByCustomTypeCustomSpELPropertyAccessQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) + .expectNextCount(1) + .verifyComplete(); } @Test void findByConvertedCustomTypeWithPropertyPlaceholderAccessQuery( @Autowired EntityWithCustomTypePropertyRepository repository) { - StepVerifier - .create(repository.findByCustomTypeCustomPropertyPlaceholderAccessQuery()) - .expectNextCount(1).verifyComplete(); + StepVerifier.create(repository.findByCustomTypeCustomPropertyPlaceholderAccessQuery()) + .expectNextCount(1) + .verifyComplete(); } @Test void findByConvertedCustomTypeWithSpELObjectQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { StepVerifier.create(repository.findByCustomTypeSpELObjectQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) - .expectNextCount(1).verifyComplete(); + .expectNextCount(1) + .verifyComplete(); } TransactionCallback createPersonWithCustomId(PersonWithCustomId.PersonId assignedId) { - return tx -> tx.run("CREATE (n:PersonWithCustomId) SET n.id = $id ", Values.parameters("id", assignedId.getId())) - .consume(); + return tx -> tx + .run("CREATE (n:PersonWithCustomId) SET n.id = $id ", Values.parameters("id", assignedId.getId())) + .consume(); } @Test void deleteByCustomId() { - PersonWithCustomId.PersonId id = new PersonWithCustomId.PersonId(customIdValueGenerator.incrementAndGet()); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + PersonWithCustomId.PersonId id = new PersonWithCustomId.PersonId(this.customIdValueGenerator.incrementAndGet()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.executeWrite(createPersonWithCustomId(id)); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - StepVerifier.create(neo4jOperations.count(PersonWithCustomId.class)).expectNext(1L).verifyComplete(); + StepVerifier.create(this.neo4jOperations.count(PersonWithCustomId.class)).expectNext(1L).verifyComplete(); - StepVerifier.create(neo4jOperations.deleteById(id, PersonWithCustomId.class)).verifyComplete(); + StepVerifier.create(this.neo4jOperations.deleteById(id, PersonWithCustomId.class)).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithCustomId) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test void deleteAllByCustomId() { - List ids = Stream.generate(customIdValueGenerator::incrementAndGet) - .map(PersonWithCustomId.PersonId::new).limit(2).collect(Collectors.toList()); - try (Session session = driver.session(bookmarkCapture.createSessionConfig());) { + List ids = Stream.generate(this.customIdValueGenerator::incrementAndGet) + .map(PersonWithCustomId.PersonId::new) + .limit(2) + .collect(Collectors.toList()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig());) { ids.forEach(id -> session.executeWrite(createPersonWithCustomId(id))); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - StepVerifier.create(neo4jOperations.count(PersonWithCustomId.class)).expectNext(2L).verifyComplete(); + StepVerifier.create(this.neo4jOperations.count(PersonWithCustomId.class)).expectNext(2L).verifyComplete(); - StepVerifier.create(neo4jOperations.deleteAllById(ids, PersonWithCustomId.class)).verifyComplete(); + StepVerifier.create(this.neo4jOperations.deleteAllById(ids, PersonWithCustomId.class)).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithCustomId) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -193,13 +202,13 @@ public class ReactiveCustomTypesIT { Mono findByCustomTypeCustomSpELPropertyAccessQuery( @Param("customType") ThingWithCustomTypes.CustomType customType); - @Query("MATCH (c:CustomTypes) WHERE c.customType = :${foo} return c") Mono findByCustomTypeCustomPropertyPlaceholderAccessQuery(); @Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType} return c") Mono findByCustomTypeSpELObjectQuery( @Param("customType") ThingWithCustomTypes.CustomType customType); + } @Configuration @@ -208,6 +217,7 @@ public class ReactiveCustomTypesIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -228,20 +238,24 @@ public class ReactiveCustomTypesIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveTypeConversionIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveTypeConversionIT.java index c54f8a2c2..59b87ae16 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveTypeConversionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveTypeConversionIT.java @@ -15,16 +15,6 @@ */ package org.springframework.data.neo4j.integration.conversion_reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,21 +23,30 @@ import java.util.UUID; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider; import org.springframework.data.neo4j.core.convert.Neo4jConversions; -import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes; import org.springframework.data.neo4j.integration.shared.common.ThingWithUUIDID; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Tom Morello - The Atlas Underground */ @Neo4jIntegrationTest @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) @@ -59,8 +58,10 @@ class ReactiveTypeConversionIT { void idsShouldBeConverted(@Autowired ConvertedIDsRepository repository) { List stored = new ArrayList<>(); - StepVerifier.create(repository.save(new ThingWithUUIDID("a thing"))).recordWith(() -> stored).expectNextCount(1L) - .verifyComplete(); + StepVerifier.create(repository.save(new ThingWithUUIDID("a thing"))) + .recordWith(() -> stored) + .expectNextCount(1L) + .verifyComplete(); StepVerifier.create(repository.findById(stored.get(0).getId())).expectNextCount(1L).verifyComplete(); } @@ -78,24 +79,28 @@ class ReactiveTypeConversionIT { assertThat(savedThing.getId()).isNotNull(); assertThat(savedThing.getAnotherThing().getId()).isNotNull(); - StepVerifier.create( - Flux.concat(repository.findById(savedThing.getId()), repository.findById(savedThing.getAnotherThing().getId()))) - .expectNextCount(2L).verifyComplete(); + StepVerifier + .create(Flux.concat(repository.findById(savedThing.getId()), + repository.findById(savedThing.getAnotherThing().getId()))) + .expectNextCount(2L) + .verifyComplete(); } @Test // GH-2594 void clientShouldUseCustomType(@Autowired ReactiveNeo4jClient client) { client.query("RETURN 'whatever'") - .fetchAs(ThingWithCustomTypes.CustomType.class) - .first() - .map(ThingWithCustomTypes.CustomType::getValue) - .as(StepVerifier::create) - .expectNext("whatever") - .verifyComplete(); + .fetchAs(ThingWithCustomTypes.CustomType.class) + .first() + .map(ThingWithCustomTypes.CustomType::getValue) + .as(StepVerifier::create) + .expectNext("whatever") + .verifyComplete(); } - public interface ConvertedIDsRepository extends ReactiveNeo4jRepository {} + public interface ConvertedIDsRepository extends ReactiveNeo4jRepository { + + } @Configuration @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) @@ -106,6 +111,7 @@ class ReactiveTypeConversionIT { private ObjectProvider userSelectionProviders; @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -121,13 +127,16 @@ class ReactiveTypeConversionIT { } @Override - public ReactiveNeo4jClient neo4jClient(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveNeo4jClient neo4jClient(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { return ReactiveNeo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(userSelectionProviders.getIfUnique()) - .withNeo4jConversions(neo4jConversions()) - .build(); + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(this.userSelectionProviders.getIfUnique()) + .withNeo4jConversions(neo4jConversions()) + .build(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingConfig.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingConfig.java new file mode 100644 index 000000000..03304e3a2 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011-2025 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.imperative; + +import java.util.Optional; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.neo4j.config.EnableNeo4jAuditing; +import org.springframework.data.neo4j.integration.shared.common.AuditingITBase; + +@Configuration +@EnableNeo4jAuditing(modifyOnCreate = false, auditorAwareRef = "auditorProvider", + dateTimeProviderRef = "fixedDateTimeProvider") +class AuditingConfig { + + @Bean + AuditorAware auditorProvider() { + return () -> Optional.of("A user"); + } + + @Bean + DateTimeProvider fixedDateTimeProvider() { + return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE); + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java index 9a3569b13..dbef56e58 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java @@ -15,25 +15,16 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; -import java.util.Optional; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; -// tag::faq.entities.auditing[] import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.data.auditing.DateTimeProvider; -import org.springframework.data.domain.AuditorAware; - -// end::faq.entities.auditing[] -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; -import org.springframework.data.neo4j.config.EnableNeo4jAuditing; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -43,9 +34,12 @@ import org.springframework.data.neo4j.integration.shared.common.ImmutableAuditab import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -74,7 +68,7 @@ class AuditingIT extends AuditingITBase { @Test void auditingOfModificationShouldWork(@Autowired ImmutableEntityTestRepository repository) { - ImmutableAuditableThing thing = repository.findById(idOfExistingThing).get(); + ImmutableAuditableThing thing = repository.findById(this.idOfExistingThing).get(); thing = thing.withName("A new name"); thing = repository.save(thing); @@ -86,7 +80,7 @@ class AuditingIT extends AuditingITBase { assertThat(thing.getName()).isEqualTo("A new name"); - verifyDatabase(idOfExistingThing, thing); + verifyDatabase(this.idOfExistingThing, thing); } @Test @@ -109,7 +103,7 @@ class AuditingIT extends AuditingITBase { void auditingOfEntityWithGeneratedIdModificationShouldWork( @Autowired ImmutableEntityWithGeneratedIdRepository repository) { - ImmutableAuditableThingWithGeneratedId thing = repository.findById(idOfExistingThingWithGeneratedId).get(); + ImmutableAuditableThingWithGeneratedId thing = repository.findById(this.idOfExistingThingWithGeneratedId).get(); thing = thing.withName("A new name"); thing = repository.save(thing); @@ -122,13 +116,17 @@ class AuditingIT extends AuditingITBase { assertThat(thing.getName()).isEqualTo("A new name"); - verifyDatabase(idOfExistingThingWithGeneratedId, thing); + verifyDatabase(this.idOfExistingThingWithGeneratedId, thing); } - interface ImmutableEntityTestRepository extends Neo4jRepository {} + interface ImmutableEntityTestRepository extends Neo4jRepository { + + } interface ImmutableEntityWithGeneratedIdRepository - extends Neo4jRepository {} + extends Neo4jRepository { + + } @Configuration @Import(AuditingConfig.class) @@ -137,6 +135,7 @@ class AuditingIT extends AuditingITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -147,41 +146,24 @@ class AuditingIT extends AuditingITBase { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } - -// tag::faq.entities.auditing[] -@Configuration -@EnableNeo4jAuditing( - modifyOnCreate = false, // <.> - auditorAwareRef = "auditorProvider", // <.> - dateTimeProviderRef = "fixedDateTimeProvider" // <.> -) -class AuditingConfig { - - @Bean - public AuditorAware auditorProvider() { - return () -> Optional.of("A user"); - } - - @Bean - public DateTimeProvider fixedDateTimeProvider() { - return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE); - } -} -// end::faq.entities.auditing[] diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingWithoutDatesIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingWithoutDatesIT.java index 4ecd02f97..01ee03620 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingWithoutDatesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingWithoutDatesIT.java @@ -15,19 +15,17 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.Optional; 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.AuditorAware; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.config.EnableNeo4jAuditing; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -37,15 +35,19 @@ import org.springframework.data.neo4j.integration.shared.common.ImmutableAuditab import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ class AuditingWithoutDatesIT extends AuditingITBase { - @Autowired AuditingWithoutDatesIT(Driver driver, BookmarkCapture bookmarkCapture) { + @Autowired + AuditingWithoutDatesIT(Driver driver, BookmarkCapture bookmarkCapture) { super(driver, bookmarkCapture); } @@ -77,7 +79,9 @@ class AuditingWithoutDatesIT extends AuditingITBase { verifyDatabase(thing.getId(), thing); } - interface ImmutableEntityTestRepository extends Neo4jRepository {} + interface ImmutableEntityTestRepository extends Neo4jRepository { + + } @Configuration @EnableNeo4jAuditing(setDates = false, auditorAwareRef = "auditorProvider") @@ -86,6 +90,7 @@ class AuditingWithoutDatesIT extends AuditingITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -96,25 +101,29 @@ class AuditingWithoutDatesIT extends AuditingITBase { } @Bean - public AuditorAware auditorProvider() { + AuditorAware auditorProvider() { return () -> Optional.of("A user"); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksConfig.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksConfig.java new file mode 100644 index 000000000..c9cbcab61 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011-2025 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.imperative; + +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback; +import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback; +import org.springframework.data.neo4j.integration.shared.common.ThingWithAssignedId; + +@Configuration +class CallbacksConfig { + + @Bean + BeforeBindCallback nameChanger() { + return entity -> { + ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId(), + entity.getName() + " (Edited)"); + return updatedThing; + }; + } + + @Bean + AfterConvertCallback randomValueAssigner() { + return (entity, definition, source) -> { + entity.setRandomValue(UUID.randomUUID().toString()); + return entity; + }; + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java index fa02d1d2b..98fb24bd9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java @@ -15,33 +15,21 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; - import java.util.Arrays; import java.util.Collections; import java.util.Objects; import java.util.Optional; -// tag::faq.entities.auditing.callbacks[] import java.util.UUID; import java.util.stream.StreamSupport; -// end::faq.entities.auditing.callbacks[] import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; -// tag::faq.entities.auditing.callbacks[] import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -// end::faq.entities.auditing.callbacks[] import org.springframework.context.annotation.Import; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; -// tag::faq.entities.auditing.callbacks[] -import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback; -import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback; - -// end::faq.entities.auditing.callbacks[] import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.integration.imperative.repositories.ThingRepository; @@ -49,15 +37,20 @@ import org.springframework.data.neo4j.integration.shared.common.CallbacksITBase; import org.springframework.data.neo4j.integration.shared.common.ThingWithAssignedId; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + /** * @author Michael J. Simons */ class CallbacksIT extends CallbacksITBase { - @Autowired CallbacksIT(Driver driver, BookmarkCapture bookmarkCapture) { + @Autowired + CallbacksIT(Driver driver, BookmarkCapture bookmarkCapture) { super(driver, bookmarkCapture); } @@ -81,9 +74,9 @@ class CallbacksIT extends CallbacksITBase { assertThat(optionalThing).hasValueSatisfying(thingWithAssignedId -> { assertThat(thingWithAssignedId.getTheId()).isEqualTo("E1"); assertThat(thingWithAssignedId.getRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); }); } @@ -93,7 +86,7 @@ class CallbacksIT extends CallbacksITBase { Optional optionalThing = repository.findById("E1"); assertThat(optionalThing).hasValueSatisfying( thingWithAssignedId -> assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v)))); + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v)))); } @Test @@ -110,11 +103,9 @@ class CallbacksIT extends CallbacksITBase { assertThat(unsaved).allMatch(v -> v.getRandomValue() != null); assertThat(unsaved).noneMatch(v -> v.getAnotherRandomValue() != null); - assertThat(savedThings).extracting(ThingWithAssignedId::getName).containsExactlyInAnyOrder("A name (Edited)", - "Another name (Edited)"); - assertThat(savedThings).hasSize(2) - .extracting(ThingWithAssignedId::getRandomValue) - .allMatch(Objects::isNull); + assertThat(savedThings).extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("A name (Edited)", "Another name (Edited)"); + assertThat(savedThings).hasSize(2).extracting(ThingWithAssignedId::getRandomValue).allMatch(Objects::isNull); // Assert the onAfterConvert var ids = StreamSupport.stream(savedThings.spliterator(), false).map(ThingWithAssignedId::getTheId).toList(); @@ -128,12 +119,11 @@ class CallbacksIT extends CallbacksITBase { void onAfterConvertShouldBeCalledForAllEntities(@Autowired ThingRepository repository) { Iterable optionalThing = repository.findAllById(Arrays.asList("E1", "E2")); - assertThat(optionalThing).hasSize(2) - .allSatisfy(thingWithAssignedId -> { - assertThat(thingWithAssignedId.getTheId()).startsWith("E"); - assertThat(thingWithAssignedId.getRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - }); + assertThat(optionalThing).hasSize(2).allSatisfy(thingWithAssignedId -> { + assertThat(thingWithAssignedId.getTheId()).startsWith("E"); + assertThat(thingWithAssignedId.getRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + }); } @Test // GH-2499 @@ -141,8 +131,8 @@ class CallbacksIT extends CallbacksITBase { Iterable optionalThing = repository.findAllById(Arrays.asList("E1", "E2")); assertThat(optionalThing).hasSize(2) - .allSatisfy(thingWithAssignedId -> assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v)))); + .allSatisfy(thingWithAssignedId -> assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v)))); } @Configuration @@ -152,12 +142,13 @@ class CallbacksIT extends CallbacksITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -174,28 +165,7 @@ class CallbacksIT extends CallbacksITBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } - -// tag::faq.entities.auditing.callbacks[] -@Configuration -class CallbacksConfig { - - @Bean - BeforeBindCallback nameChanger() { - return entity -> { - ThingWithAssignedId updatedThing = new ThingWithAssignedId( - entity.getTheId(), entity.getName() + " (Edited)"); - return updatedThing; - }; - } - - @Bean - AfterConvertCallback randomValueAssigner() { - return (entity, definition, source) -> { - entity.setRandomValue(UUID.randomUUID().toString()); - return entity; - }; - } -} -// end::faq.entities.auditing.callbacks[] diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CausalClusterLoadTestIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CausalClusterLoadTestIT.java index 91c4af7a3..c63a8c8e6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CausalClusterLoadTestIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CausalClusterLoadTestIT.java @@ -28,7 +28,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.neo4j.driver.AuthTokens; @@ -41,20 +40,23 @@ import org.neo4j.junit.jupiter.causal_cluster.CausalCluster; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.integration.shared.common.ThingWithSequence; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.CausalClusterIntegrationTest; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.ServerVersion; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.fail; + /** - * This tests needs a Neo4j causal cluster. We run them based on Testcontainers. It requires some resources as well as - * acceptance of the commercial license, so this test is disabled by default. + * This tests needs a Neo4j causal cluster. We run them based on Testcontainers. It + * requires some resources as well as acceptance of the commercial license, so this test + * is disabled by default. * * @author Michael J. Simons */ @@ -62,7 +64,8 @@ import org.springframework.transaction.annotation.Transactional; @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) class CausalClusterLoadTestIT { - @CausalCluster private static URI neo4jUri; + @CausalCluster + private static URI neo4jUri; @RepeatedTest(20) void transactionsShouldBeSerializable(@Autowired ThingService thingService) throws InterruptedException { @@ -72,31 +75,39 @@ class CausalClusterLoadTestIT { Callable createAndRead = () -> { ThingWithSequence newThing = thingService.newThing(sequence.incrementAndGet()); - Optional optionalThing = thingService.findOneBySequenceNumber(newThing.getSequenceNumber()); + Optional optionalThing = thingService + .findOneBySequenceNumber(newThing.getSequenceNumber()); return optionalThing.orElseThrow(() -> new RuntimeException("Did not read my own write :(")); }; ExecutorService executor = Executors.newCachedThreadPool(); List> executedWrites = executor - .invokeAll(IntStream.range(0, numberOfRequests).mapToObj(i -> createAndRead).collect(Collectors.toList())); + .invokeAll(IntStream.range(0, numberOfRequests).mapToObj(i -> createAndRead).collect(Collectors.toList())); try { executedWrites.forEach(request -> { try { request.get(); - } catch (InterruptedException e) {} catch (ExecutionException e) { - Assertions.fail("At least one request failed " + e.getMessage()); + } + catch (InterruptedException ex) { + } + catch (ExecutionException ex) { + fail("At least one request failed " + ex.getMessage()); } }); - } finally { + } + finally { executor.shutdown(); } } interface ThingRepository extends Neo4jRepository { + Optional findOneBySequenceNumber(long sequenceNumber); + } static class ThingService { + private final Neo4jClient neo4jClient; private final ThingRepository thingRepository; @@ -106,20 +117,24 @@ class CausalClusterLoadTestIT { this.thingRepository = thingRepository; } - public long getMaxInstance() { - return neo4jClient.query("MATCH (t:ThingWithSequence) RETURN COALESCE(MAX(t.sequenceNumber), -1) AS maxInstance") - .fetchAs(Long.class).one().get(); + long getMaxInstance() { + return this.neo4jClient + .query("MATCH (t:ThingWithSequence) RETURN COALESCE(MAX(t.sequenceNumber), -1) AS maxInstance") + .fetchAs(Long.class) + .one() + .get(); } @Transactional - public ThingWithSequence newThing(long i) { + ThingWithSequence newThing(long i) { return this.thingRepository.save(new ThingWithSequence(i)); } @Transactional(readOnly = true) - public Optional findOneBySequenceNumber(long sequenceNumber) { - return thingRepository.findOneBySequenceNumber(sequenceNumber); + Optional findOneBySequenceNumber(long sequenceNumber) { + return this.thingRepository.findOneBySequenceNumber(sequenceNumber); } + } @Configuration @@ -128,6 +143,7 @@ class CausalClusterLoadTestIT { static class TestConfig extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { Driver driver = GraphDatabase.driver(neo4jUri, AuthTokens.basic("neo4j", "secret"), @@ -137,20 +153,23 @@ class CausalClusterLoadTestIT { } @Bean - public ThingService thingService(Neo4jClient neo4jClient, ThingRepository thingRepository) { + ThingService thingService(Neo4jClient neo4jClient, ThingRepository thingRepository) { return new ThingService(neo4jClient, thingRepository); } @Override public boolean isCypher5Compatible() { try (Session session = driver().session()) { - String version = session - .run("CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version") - .single() - .get("version").asString(); + String version = session.run( + "CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version") + .single() + .get("version") + .asString(); return ServerVersion.version(version).greaterThanOrEqual(ServerVersion.v4_4_0); } } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ChainedAuditingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ChainedAuditingIT.java index 75cf384da..6905bc761 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ChainedAuditingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ChainedAuditingIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.Optional; @@ -27,6 +25,7 @@ import java.util.stream.Stream; 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; @@ -50,6 +49,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -96,6 +97,7 @@ public class ChainedAuditingIT { } interface BookRepository extends Neo4jRepository { + } static class BookEditorHistorian implements BeforeBindCallback, Ordered { @@ -116,6 +118,7 @@ public class ChainedAuditingIT { public int getOrder() { return AuditingBeforeBindCallback.NEO4J_AUDITING_ORDER - 50; } + } @Configuration @@ -125,6 +128,7 @@ public class ChainedAuditingIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -135,7 +139,7 @@ public class ChainedAuditingIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -154,7 +158,7 @@ public class ChainedAuditingIT { } @Bean - public AuditorAware auditorProvider() { + AuditorAware auditorProvider() { var state = new AtomicInteger(0); return () -> { int i = state.compareAndSet(3, 4) ? 3 : state.incrementAndGet(); @@ -163,9 +167,10 @@ public class ChainedAuditingIT { } @Bean - public BeforeBindCallback bookEditorHistorian() { + BeforeBindCallback bookEditorHistorian() { return new BookEditorHistorian(); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CollectionsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CollectionsIT.java index 802b7eca5..1ff24f286 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CollectionsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CollectionsIT.java @@ -15,13 +15,19 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.convert.Neo4jConversions; @@ -36,16 +42,11 @@ import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -70,11 +71,13 @@ public class CollectionsIT { void loadingOfRelPropertiesInSetsShouldWork(@Autowired Neo4jTemplate repository) { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { id = session.run( - "CREATE (c:CollectionChildNodeA {name: 'The Child'}) <- [:CHILDREN_WITH_PROPERTIES {prop: 'The Property'}] - (p:CollectionParentNode {name: 'The Parent'}) RETURN id(p)" - ).single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + "CREATE (c:CollectionChildNodeA {name: 'The Child'}) <- [:CHILDREN_WITH_PROPERTIES {prop: 'The Property'}] - (p:CollectionParentNode {name: 'The Parent'}) RETURN id(p)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } Optional optionalParent = repository.findById(id, CollectionParentNode.class); @@ -106,62 +109,66 @@ public class CollectionsIT { assertThat(p.prop).isEqualTo("a property"); }); - - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long cnt = session.run( "MATCH (c:CollectionChildNodeA) <- [:CHILDREN_WITH_PROPERTIES] - (p:CollectionParentNode) WHERE id(p) = $id RETURN count(c) ", - Collections.singletonMap("id", parent.id) - ).single().get(0).asLong(); + Collections.singletonMap("id", parent.id)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Node static class CollectionParentNode { + final String name; + @Id @GeneratedValue Long id; - final String name; - Set childrenWithProperties = new HashSet<>(); CollectionParentNode(String name) { this.name = name; } + } @Node static class CollectionChildNodeA { + final String name; + @Id @GeneratedValue Long id; - final String name; - CollectionChildNodeA(String name) { this.name = name; } + } @RelationshipProperties static class RelProperties { - @RelationshipId - Long id; - @TargetNode final CollectionChildNodeA target; final String prop; + @RelationshipId + Long id; + RelProperties(CollectionChildNodeA target, String prop) { this.target = target; this.prop = prop; } + } @Configuration @@ -169,36 +176,44 @@ public class CollectionsIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Override - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { // Don't create repositories for the entities, otherwise they must be moved - // to a public reachable place. I didn't want that as the mapping context is polluted already + // to a public reachable place. I didn't want that as the mapping context is + // polluted already // enough with the shared package of nodes. Neo4jMappingContext ctx = new Neo4jMappingContext(neo4JConversions); - ctx.setInitialEntitySet(new HashSet<>(Arrays.asList(CollectionParentNode.class, CollectionChildNodeA.class, RelProperties.class))); + ctx.setInitialEntitySet(new HashSet<>( + Arrays.asList(CollectionParentNode.class, CollectionChildNodeA.class, RelProperties.class))); return ctx; } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CustomBaseRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CustomBaseRepositoryIT.java index d9a2fb208..b71204202 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CustomBaseRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CustomBaseRepositoryIT.java @@ -15,21 +15,18 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor; import org.springframework.data.neo4j.repository.Neo4jRepository; @@ -37,9 +34,13 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; import org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository; import org.springframework.data.neo4j.test.DriverMocks; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * Make sure custom base repositories can be used. * @@ -52,30 +53,29 @@ public class CustomBaseRepositoryIT { public void customBaseRepositoryShouldBeInUse(@Autowired MyPersonRepository repository) { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()) - .withMessage("This implementation does not support `findAll`"); + .withMessage("This implementation does not support `findAll`"); } - interface MyPersonRepository extends Neo4jRepository {} + interface MyPersonRepository extends Neo4jRepository { + + } /** * Used in the FAQ as well + * * @param Type of the entity * @param Type of the id */ - static // tag::custom-base-repository[] - public class MyRepositoryImpl extends SimpleNeo4jRepository { + public static class MyRepositoryImpl extends SimpleNeo4jRepository { - MyRepositoryImpl( - Neo4jOperations neo4jOperations, - Neo4jEntityInformation entityInformation - ) { + MyRepositoryImpl(Neo4jOperations neo4jOperations, Neo4jEntityInformation entityInformation) { super(neo4jOperations, entityInformation); // <.> // end::custom-base-repository[] assertThat(neo4jOperations).isNotNull(); assertThat(entityInformation).isNotNull(); Assertions.assertThat(entityInformation.getEntityMetaData().getUnderlyingClass()) - .isEqualTo(PersonWithAllConstructor.class); + .isEqualTo(PersonWithAllConstructor.class); // tag::custom-base-repository[] } @@ -83,6 +83,7 @@ public class CustomBaseRepositoryIT { public List findAll() { throw new UnsupportedOperationException("This implementation does not support `findAll`"); } + } // end::custom-base-repository[] @@ -93,6 +94,7 @@ public class CustomBaseRepositoryIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return DriverMocks.withOpenSessionAndTransaction(); } @@ -101,5 +103,7 @@ public class CustomBaseRepositoryIT { public boolean isCypher5Compatible() { return false; // does not matter } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java index 3ce1ebf39..e5cbabdbb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.cypherdsl.core.Cypher; @@ -25,31 +23,29 @@ import org.neo4j.cypherdsl.core.Property; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; 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.integration.shared.common.Person; -// tag::sdn-mixins.dynamic-conditions.add-mixin[] import org.springframework.data.neo4j.repository.Neo4jRepository; -// end::sdn-mixins.dynamic-conditions.add-mixin[] import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; -// tag::sdn-mixins.dynamic-conditions.add-mixin[] import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor; - -// end::sdn-mixins.dynamic-conditions.add-mixin[] import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -59,18 +55,19 @@ class CypherdslConditionExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Property firstName; + private final Property lastName; @Autowired CypherdslConditionExecutorIT() { - //CHECKSTYLE:OFF + // CHECKSTYLE:OFF // tag::sdn-mixins.dynamic-conditions.usage[] Node person = Cypher.node("Person").named("person"); // <.> Property firstName = person.property("firstName"); // <.> Property lastName = person.property("lastName"); // end::sdn-mixins.dynamic-conditions.usage[] - //CHECKSTYLE:ON + // CHECKSTYLE:ON this.firstName = firstName; this.lastName = lastName; @@ -79,12 +76,12 @@ class CypherdslConditionExecutorIT { @BeforeAll protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction()) { + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -94,27 +91,27 @@ class CypherdslConditionExecutorIT { @Test void findOneShouldWork(@Autowired PersonRepository repository) { - assertThat(repository.findOne(firstName.eq(Cypher.literalOf("Helge")))) - .hasValueSatisfying(p -> assertThat(p).extracting(Person::getLastName).isEqualTo("Schneider")); + assertThat(repository.findOne(this.firstName.eq(Cypher.literalOf("Helge")))) + .hasValueSatisfying(p -> assertThat(p).extracting(Person::getLastName).isEqualTo("Schneider")); } @Test void findAllShouldWork(@Autowired PersonRepository repository) { - assertThat(repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))))) - .extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela", "Helge"); + assertThat(repository + .findAll(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))))) + .extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela", "Helge"); } @Test void sortedFindAllShouldWork(@Autowired PersonRepository repository) { - assertThat( - repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - Sort.by("lastName").descending() - )) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(repository.findAll( + this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending())) + .extracting(Person::getFirstName) + .containsExactly("Helge", "Bela"); } @Test @@ -122,77 +119,69 @@ class CypherdslConditionExecutorIT { // tag::sdn-mixins.dynamic-conditions.usage[] - assertThat( - repository.findAll( - firstName.eq(Cypher.anonParameter("Helge")) - .or(lastName.eq(Cypher.parameter("someName", "B."))), // <.> - lastName.descending() // <.> - )) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(repository.findAll( + this.firstName.eq(Cypher.anonParameter("Helge")) + .or(this.lastName.eq(Cypher.parameter("someName", "B."))), // <.> + this.lastName.descending() // <.> + )).extracting(Person::getFirstName).containsExactly("Helge", "Bela"); // end::sdn-mixins.dynamic-conditions.usage[] } @Test void orderedFindAllShouldWork(@Autowired PersonRepository repository) { - assertThat( - repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - Sort.by("lastName").descending() - )) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(repository.findAll( + this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending())) + .extracting(Person::getFirstName) + .containsExactly("Helge", "Bela"); } @Test void orderedFindAllWithoutPredicateShouldWork(@Autowired PersonRepository repository) { - assertThat(repository.findAll(lastName.descending())) - .extracting(Person::getFirstName) - .containsExactly("Helge", "B", "A", "Bela"); + assertThat(repository.findAll(this.lastName.descending())).extracting(Person::getFirstName) + .containsExactly("Helge", "B", "A", "Bela"); } @Test void pagedFindAllShouldWork(@Autowired PersonRepository repository) { - Page people = repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - PageRequest.of(1, 1, Sort.by("lastName").descending()) - ); + Page people = repository.findAll( + this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + PageRequest.of(1, 1, Sort.by("lastName").descending())); assertThat(people.hasPrevious()).isTrue(); assertThat(people.hasNext()).isFalse(); assertThat(people.getTotalElements()).isEqualTo(2); - assertThat(people) - .extracting(Person::getFirstName) - .containsExactly("Bela"); + assertThat(people).extracting(Person::getFirstName).containsExactly("Bela"); } @Test // GH-2194 void pagedFindAllShouldWork2(@Autowired PersonRepository repository) { - Page people = repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - PageRequest.of(0, 20, Sort.by("lastName").descending()) - ); + Page people = repository.findAll( + this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + PageRequest.of(0, 20, Sort.by("lastName").descending())); assertThat(people.hasPrevious()).isFalse(); assertThat(people.hasNext()).isFalse(); assertThat(people.getTotalElements()).isEqualTo(2); - assertThat(people) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge", "Bela"); } @Test void countShouldWork(@Autowired PersonRepository repository) { - assertThat(repository.count(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))))) - .isEqualTo(2L); + assertThat(repository + .count(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))))) + .isEqualTo(2L); } @Test void existsShouldWork(@Autowired PersonRepository repository) { - assertThat(repository.exists(firstName.eq(Cypher.literalOf("A")))).isTrue(); + assertThat(repository.exists(this.firstName.eq(Cypher.literalOf("A")))).isTrue(); } @Test // GH-2261 @@ -202,15 +191,19 @@ class CypherdslConditionExecutorIT { } // tag::sdn-mixins.dynamic-conditions.add-mixin[] - interface PersonRepository extends - Neo4jRepository, // <.> - CypherdslConditionExecutor { // <.> + interface PersonRepository extends Neo4jRepository, // <.> + CypherdslConditionExecutor { + + // <.> + } // end::sdn-mixins.dynamic-conditions.add-mixin[] - interface ARepositoryWithDerivedFinderMethods extends Neo4jRepository, CypherdslConditionExecutor { + interface ARepositoryWithDerivedFinderMethods + extends Neo4jRepository, CypherdslConditionExecutor { Person findByFirstName(String firstName); + } @Configuration @@ -219,26 +212,31 @@ class CypherdslConditionExecutorIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java index 100300cc2..c0597b9f1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Optional; import org.junit.jupiter.api.BeforeAll; @@ -28,13 +26,13 @@ import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -45,10 +43,13 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.support.CypherdslStatementExecutor; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -63,8 +64,8 @@ class CypherdslStatementExecutorIT { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); } @@ -72,8 +73,10 @@ class CypherdslStatementExecutorIT { static Statement whoHasFirstName(String name) { Node p = Cypher.node("Person").named("p"); - return Cypher.match(p).where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))).returning(p) - .build(); + return Cypher.match(p) + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) + .returning(p) + .build(); } // tag::sdn-mixins.using-cypher-dsl-statements.using[] @@ -82,13 +85,9 @@ class CypherdslStatementExecutorIT { Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); return Cypher.match(r) - .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) // <.> - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ) - .build(); + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) // <.> + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)) + .build(); } // end::sdn-mixins.using-cypher-dsl-statements.using[] @@ -96,26 +95,20 @@ class CypherdslStatementExecutorIT { Node p = Cypher.node("Person").named("p"); Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); - return Cypher.match(p).optionalMatch(r) - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ) - .orderBy(p.property("firstName").ascending()) - .build(); + return Cypher.match(p) + .optionalMatch(r) + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)) + .orderBy(p.property("firstName").ascending()) + .build(); } static OngoingReadingAndReturn byCustomQueryWithoutOrder() { Node p = Cypher.node("Person").named("p"); Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); - return Cypher.match(p).optionalMatch(r) - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ); + return Cypher.match(p) + .optionalMatch(r) + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)); } @Test @@ -130,13 +123,12 @@ class CypherdslStatementExecutorIT { @Test void fineOneShouldWork(@Autowired PersonRepository repository) { - Optional result = repository.findOne(whoHasFirstNameWithAddress("Helge")); // <.> + Optional result = repository.findOne(whoHasFirstNameWithAddress("Helge")); // <.> assertThat(result).hasValueSatisfying(namesOnly -> { assertThat(namesOnly.getFirstName()).isEqualTo("Helge"); assertThat(namesOnly.getLastName()).isEqualTo("Schneider"); - assertThat(namesOnly.getAddress()).extracting(Person.Address::getCity) - .isEqualTo("Mülheim an der Ruhr"); + assertThat(namesOnly.getAddress()).extracting(Person.Address::getCity).isEqualTo("Mülheim an der Ruhr"); }); } @@ -153,9 +145,7 @@ class CypherdslStatementExecutorIT { @Test void fineOneProjectedShouldWork(@Autowired PersonRepository repository) { - Optional result = repository.findOne( - whoHasFirstNameWithAddress("Helge"), - NamesOnly.class // <.> + Optional result = repository.findOne(whoHasFirstNameWithAddress("Helge"), NamesOnly.class // <.> ); assertThat(result).hasValueSatisfying(namesOnly -> { @@ -171,9 +161,7 @@ class CypherdslStatementExecutorIT { Iterable result = repository.findAll(byCustomQuery()); - assertThat(result) - .extracting(Person::getFirstName) - .containsExactly("A", "B", "Bela", "Helge"); + assertThat(result).extracting(Person::getFirstName).containsExactly("A", "B", "Bela", "Helge"); assertThat(result).anySatisfy(p -> assertThat(p.getAddress()).isNotNull()); } @@ -182,42 +170,34 @@ class CypherdslStatementExecutorIT { Iterable result = repository.findAll(byCustomQuery(), NamesOnly.class); - assertThat(result) - .extracting(NamesOnly::getFullName) - .containsExactly("A LA", "B LB", "Bela B.", "Helge Schneider"); + assertThat(result).extracting(NamesOnly::getFullName) + .containsExactly("A LA", "B LB", "Bela B.", "Helge Schneider"); } @Test void findPageShouldWork(@Autowired PersonRepository repository) { Node person = Cypher.node("Person"); - Page result = repository.findAll( - byCustomQueryWithoutOrder(), Cypher.match(person).returning(Cypher.count(person)).build(), - PageRequest.of(1, 2, Sort.by("p.firstName").ascending()) - ); + Page result = repository.findAll(byCustomQueryWithoutOrder(), + Cypher.match(person).returning(Cypher.count(person)).build(), + PageRequest.of(1, 2, Sort.by("p.firstName").ascending())); assertThat(result.hasPrevious()).isTrue(); assertThat(result.hasNext()).isFalse(); - assertThat(result) - .extracting(Person::getFirstName) - .containsExactly("Bela", "Helge"); + assertThat(result).extracting(Person::getFirstName).containsExactly("Bela", "Helge"); } @Test void findPageProjectedShouldWork(@Autowired PersonRepository repository) { Node person = Cypher.node("Person"); - Page result = repository.findAll( - byCustomQueryWithoutOrder(), Cypher.match(person).returning(Cypher.count(person)).build(), - PageRequest.of(1, 2, Sort.by("p.firstName").ascending()), - NamesOnly.class - ); + Page result = repository.findAll(byCustomQueryWithoutOrder(), + Cypher.match(person).returning(Cypher.count(person)).build(), + PageRequest.of(1, 2, Sort.by("p.firstName").ascending()), NamesOnly.class); assertThat(result.hasPrevious()).isTrue(); assertThat(result.hasNext()).isFalse(); - assertThat(result) - .extracting(NamesOnly::getFullName) - .containsExactly("Bela B.", "Helge Schneider"); + assertThat(result).extracting(NamesOnly::getFullName).containsExactly("Bela B.", "Helge Schneider"); } @Test // GH-2261 @@ -227,16 +207,16 @@ class CypherdslStatementExecutorIT { } // tag::sdn-mixins.using-cypher-dsl-statements.add-mixin[] - interface PersonRepository extends - Neo4jRepository, - CypherdslStatementExecutor { + interface PersonRepository extends Neo4jRepository, CypherdslStatementExecutor { + } // end::sdn-mixins.using-cypher-dsl-statements.add-mixin[] - interface ARepositoryWithDerivedFinderMethods extends Neo4jRepository, - CypherdslStatementExecutor { + interface ARepositoryWithDerivedFinderMethods + extends Neo4jRepository, CypherdslStatementExecutor { Person findByFirstName(String firstName); + } @Configuration @@ -245,26 +225,31 @@ class CypherdslStatementExecutorIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java index e05dc999b..820279edb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -38,13 +35,10 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.TransactionContext; import org.neo4j.driver.Value; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.integration.shared.common.Port; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.config.Neo4jEntityScanner; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; @@ -64,8 +58,12 @@ import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDyna import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SimpleDynamicLabelsWithVersion; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SuperNode; import org.springframework.data.neo4j.integration.shared.common.EntityWithDynamicLabelsAndIdThatNeedsToBeConverted; +import org.springframework.data.neo4j.integration.shared.common.Port; +import org.springframework.data.neo4j.repository.Neo4jRepository; +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.Neo4jImperativeTestConfiguration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -73,29 +71,157 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.neo4j.cypherdsl.core.Cypher.parameter; + /** * @author Michael J. Simons - * @soundtrack Samy Deluxe - Samy Deluxe */ @ExtendWith(Neo4jExtension.class) -public class DynamicLabelsIT { +final class DynamicLabelsIT { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private DynamicLabelsIT() { + } + + interface PortRepository extends Neo4jRepository { + + List findByLabelsContaining(String label); + + } + + interface AbstractBaseEntityWithDynamicLabelsRepository + extends Neo4jRepository { + + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = SpringTestBase.Config.class) + @DirtiesContext + abstract static class SpringTestBase { + + @Autowired + protected Driver driver; + + @Autowired + protected TransactionTemplate transactionTemplate; + + @Autowired + protected BookmarkCapture bookmarkCapture; + + protected Long existingEntityId; + + abstract Long createTestEntity(TransactionContext ctx); + + T executeInTransaction(Callable runnable) { + return this.transactionTemplate.execute(tx -> { + try { + return runnable.call(); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + + @BeforeEach + void setupData() { + try (Session session = this.driver.session()) { + session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); + this.existingEntityId = session.executeWrite(this::createTestEntity); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + + protected final List getLabels(Long id) { + return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(parameter("id")), id); + } + + protected final List getLabels(Condition idCondition, Object id) { + + Node n = Cypher.anyNode("n"); + String cypher = Renderer.getDefaultRenderer() + .render(Cypher.match(n) + .where(idCondition) + .and(n.property("moreLabels").isNull()) + .returning(n.labels().as("labels")) + .build()); + + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + return session.executeRead(tx -> tx.run(cypher, Collections.singletonMap("id", id)) + .single() + .get("labels") + .asList(Value::asString)); + } + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends Neo4jImperativeTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + 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 + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + return new TransactionTemplate(transactionManager); + } + + @Bean + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); + mappingContext.setInitialEntitySet( + Neo4jEntityScanner.get().scan(EntitiesWithDynamicLabels.class.getPackage().getName())); + return mappingContext; + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + + } @Nested class EntityWithSingleStaticLabelAndGeneratedId extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + Record r = transaction.run( + "CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, SimpleDynamicLabels.class); + Optional optionalEntity = template.findById(this.existingEntityId, + SimpleDynamicLabels.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); } @@ -104,14 +230,15 @@ public class DynamicLabelsIT { void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - SimpleDynamicLabels entity = template.findById(existingEntityId, SimpleDynamicLabels.class).get(); + SimpleDynamicLabels entity = template.findById(this.existingEntityId, SimpleDynamicLabels.class).get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", + "Bar", "Baz", "Foobar"); } @Test @@ -146,6 +273,7 @@ public class DynamicLabelsIT { List labels = getLabels(id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "A", "B", "C"); } + } @Nested @@ -153,16 +281,16 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); @@ -172,15 +300,17 @@ public class DynamicLabelsIT { void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - InheritedSimpleDynamicLabels entity = template.findById(existingEntityId, InheritedSimpleDynamicLabels.class) - .get(); + InheritedSimpleDynamicLabels entity = template + .findById(this.existingEntityId, InheritedSimpleDynamicLabels.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", + "Bar", "Baz", "Foobar"); } @Test @@ -196,8 +326,10 @@ public class DynamicLabelsIT { }); List labels = getLabels(id); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "A", "B", "C"); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "A", + "B", "C"); } + } @Nested @@ -206,9 +338,9 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run(""" - CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) - RETURN id(e) as existingEntityId - """).single(); + CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) + RETURN id(e) as existingEntityId + """).single(); return r.get("existingEntityId").asLong(); } @@ -216,15 +348,17 @@ public class DynamicLabelsIT { void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - SimpleDynamicLabelsWithBusinessId entity = template.findById("E1", SimpleDynamicLabelsWithBusinessId.class) - .get(); + SimpleDynamicLabelsWithBusinessId entity = template + .findById("E1", SimpleDynamicLabelsWithBusinessId.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "Fizz", "Bar", "Baz", + "Foobar"); } @Test @@ -243,6 +377,7 @@ public class DynamicLabelsIT { List labels = getLabels(Cypher.anyNode("n").property("id").isEqualTo(parameter("id")), result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "A", "B", "C"); } + } @Nested @@ -250,8 +385,10 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) " - + "RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) " + + "RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -260,34 +397,36 @@ public class DynamicLabelsIT { SimpleDynamicLabelsWithVersion result = executeInTransaction(() -> { SimpleDynamicLabelsWithVersion entity = template - .findById(existingEntityId, SimpleDynamicLabelsWithVersion.class) - .get(); + .findById(this.existingEntityId, SimpleDynamicLabelsWithVersion.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(1); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "Fizz", "Bar", "Baz", + "Foobar"); } @Test void shouldWriteDynamicLabels(@Autowired Neo4jTemplate template) { SimpleDynamicLabelsWithVersion result = executeInTransaction(() -> { - SimpleDynamicLabelsWithVersion entity = new SimpleDynamicLabelsWithVersion(); - entity.moreLabels = new HashSet<>(); - entity.moreLabels.add("A"); - entity.moreLabels.add("B"); - entity.moreLabels.add("C"); - return template.save(entity); + SimpleDynamicLabelsWithVersion entity = new SimpleDynamicLabelsWithVersion(); + entity.moreLabels = new HashSet<>(); + entity.moreLabels.add("A"); + entity.moreLabels.add("B"); + entity.moreLabels.add("C"); + return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(0); List labels = getLabels(result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "A", "B", "C"); } + } @Nested @@ -295,8 +434,9 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -305,17 +445,17 @@ public class DynamicLabelsIT { SimpleDynamicLabelsWithBusinessIdAndVersion result = executeInTransaction(() -> { SimpleDynamicLabelsWithBusinessIdAndVersion entity = template - .findById("E2", SimpleDynamicLabelsWithBusinessIdAndVersion.class).get(); + .findById("E2", SimpleDynamicLabelsWithBusinessIdAndVersion.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(1); - List labels = getLabels(existingEntityId); - assertThat(labels) - .containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "Fizz", "Bar", "Baz", - "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "Fizz", "Bar", + "Baz", "Foobar"); } @Test @@ -335,6 +475,7 @@ public class DynamicLabelsIT { List labels = getLabels(Cypher.anyNode("n").property("id").isEqualTo(parameter("id")), result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "A", "B", "C"); } + } @Nested @@ -343,19 +484,20 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, SimpleDynamicLabelsCtor.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); } + } @Nested @@ -364,25 +506,26 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsOnClassWithSingleNodeLabel(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, DynamicLabelsWithNodeLabel.class); assertThat(optionalEntity).hasValueSatisfying(entity -> assertThat(entity.moreLabels) - .containsExactlyInAnyOrder("SimpleDynamicLabels", "Foo", "Bar", "Foobar")); + .containsExactlyInAnyOrder("SimpleDynamicLabels", "Foo", "Bar", "Foobar")); } @Test void shouldReadDynamicLabelsOnClassWithMultipleNodeLabel(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, DynamicLabelsWithMultipleNodeLabels.class); - assertThat(optionalEntity).hasValueSatisfying( - entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("SimpleDynamicLabels", "Baz", "Foobar")); + assertThat(optionalEntity).hasValueSatisfying(entity -> assertThat(entity.moreLabels) + .containsExactlyInAnyOrder("SimpleDynamicLabels", "Baz", "Foobar")); } @Test // GH-2296 @@ -390,15 +533,16 @@ public class DynamicLabelsIT { template.deleteAll(EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); EntityWithDynamicLabelsAndIdThatNeedsToBeConverted savedInstance = template - .save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted("value_1")); + .save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted("value_1")); assertThat(savedInstance.getValue()).isEqualTo("value_1"); assertThat(savedInstance.getExtraLabels()).containsExactlyInAnyOrder("value_1"); - Optional optionalReloadedInstance = - template.findById(savedInstance.getId(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); + Optional optionalReloadedInstance = template + .findById(savedInstance.getId(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); assertThat(optionalReloadedInstance).hasValueSatisfying(v -> v.getExtraLabels().contains("value_1")); } + } @Nested @@ -406,18 +550,21 @@ public class DynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction + .run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsInInheritance(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, ExtendedBaseClass1.class); - assertThat(optionalEntity) - .hasValueSatisfying(entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("D1", "D2", "D3")); + Optional optionalEntity = template.findById(this.existingEntityId, + ExtendedBaseClass1.class); + assertThat(optionalEntity).hasValueSatisfying( + entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("D1", "D2", "D3")); } + } @Nested @@ -430,16 +577,16 @@ public class DynamicLabelsIT { @Test void instantiateConcreteEntityType(@Autowired AbstractBaseEntityWithDynamicLabelsRepository repository) { - EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels entity = - new EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels entity = new EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels(); entity.labels = Collections.singleton("AdditionalLabel"); entity.name = "Name"; entity.id = "ID1"; repository.save(entity); - EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels loadedEntity = - (EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels) repository.findById("ID1").get(); + EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels loadedEntity = (EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels) repository + .findById("ID1") + .get(); assertThat(loadedEntity.labels).contains("AdditionalLabel"); } @@ -464,103 +611,7 @@ public class DynamicLabelsIT { List ports = portRepository.findByLabelsContaining("A"); assertThat(ports).hasSize(2); } + } - interface PortRepository extends Neo4jRepository { - List findByLabelsContaining(String label); - } - - interface AbstractBaseEntityWithDynamicLabelsRepository extends Neo4jRepository {} - - @ExtendWith(SpringExtension.class) - @ContextConfiguration(classes = SpringTestBase.Config.class) - @DirtiesContext - abstract static class SpringTestBase { - - @Autowired protected Driver driver; - - @Autowired protected TransactionTemplate transactionTemplate; - - @Autowired protected BookmarkCapture bookmarkCapture; - - protected Long existingEntityId; - - abstract Long createTestEntity(TransactionContext ctx); - - T executeInTransaction(Callable runnable) { - return transactionTemplate.execute(tx -> { - try { - return runnable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @BeforeEach - void setupData() { - try (Session session = driver.session()) { - session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); - existingEntityId = session.executeWrite(this::createTestEntity); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - - protected final List getLabels(Long id) { - return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(parameter("id")), id); - } - - protected final List getLabels(Condition idCondition, Object id) { - - Node n = Cypher.anyNode("n"); - String cypher = Renderer.getDefaultRenderer().render(Cypher.match(n).where(idCondition) - .and(n.property("moreLabels").isNull()).returning(n.labels().as("labels")).build()); - - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - return session.executeRead( - tx -> tx.run(cypher, Collections.singletonMap("id", id)).single().get("labels").asList(Value::asString)); - } - } - - @Configuration - @EnableTransactionManagement - @EnableNeo4jRepositories(considerNestedRepositories = true) - static class Config extends Neo4jImperativeTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @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 TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { - return new TransactionTemplate(transactionManager); - } - - @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { - - Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); - mappingContext.setInitialEntitySet(Neo4jEntityScanner.get().scan(EntitiesWithDynamicLabels.class.getPackage().getName())); - return mappingContext; - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicRelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicRelationshipsIT.java index f707caa37..4b6f4326f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicRelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicRelationshipsIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,10 +24,10 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -49,12 +46,16 @@ import org.springframework.data.neo4j.integration.shared.common.Pet; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -68,7 +69,7 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase> hobbies = person.getHobbies(); - assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance).containsExactly("average"); - assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getHobby).extracting(Hobby::getName).containsExactly("Biking"); + assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance) + .containsExactly("average"); + assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Biking"); } @Test // DATAGRAPH-1449 void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { - PersonWithRelatives person = repository.findById(idOfExistingPerson).get(); + PersonWithRelatives person = repository.findById(this.idOfExistingPerson).get(); assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -126,7 +130,7 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase clubs = person.getClubs(); assertThat(clubs).containsOnlyKeys(TypeOfClub.FOOTBALL); assertThat(clubs.get(TypeOfClub.FOOTBALL).getPlace()).isEqualTo("Braunschweig"); @@ -155,7 +160,7 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -252,8 +265,8 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase fish = pets.computeIfAbsent(TypeOfPet.FISH, s -> new ArrayList<>()); fish.add(new Pet("Nemo")); - List hobbyRelationships = hobbies - .computeIfAbsent(TypeOfHobby.ACTIVE, s -> new ArrayList<>()); + List hobbyRelationships = hobbies.computeIfAbsent(TypeOfHobby.ACTIVE, + s -> new ArrayList<>()); HobbyRelationship hobbyRelationship = new HobbyRelationship("ok"); Hobby hobby1 = new Hobby(); hobby1.setName("Football"); @@ -272,16 +285,21 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(3L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -289,7 +307,7 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase> hobbies = person.getHobbies(); - assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance).containsExactly("average"); - assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getHobby).extracting(Hobby::getName).containsExactly("Biking"); + assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance) + .containsExactly("average"); + assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Biking"); } interface PersonWithRelativesRepository extends CrudRepository { @@ -334,25 +355,30 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})").run()).withMessageMatching( - "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); + .isThrownBy(() -> neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})").run()) + .withMessageMatching( + "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); } @Test @@ -102,12 +104,14 @@ class ExceptionTranslationIT { repository.save(new SimplePerson("Jerry")); assertThatExceptionOfType(DataIntegrityViolationException.class) - .isThrownBy(() -> repository.save(new SimplePerson("Jerry"))).withMessageMatching( - "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); + .isThrownBy(() -> repository.save(new SimplePerson("Jerry"))) + .withMessageMatching( + "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); } /* - * Only when an additional {@link PersistenceExceptionTranslationPostProcessor} has been provided. + * Only when an additional {@link PersistenceExceptionTranslationPostProcessor} has + * been provided. */ @Test void exceptionsOnRepositoryBeansShouldBeTranslated(@Autowired CustomDAO customDAO) { @@ -115,11 +119,13 @@ class ExceptionTranslationIT { assertThat(summary.counters().nodesCreated()).isEqualTo(1L); assertThatExceptionOfType(DataIntegrityViolationException.class).isThrownBy(() -> customDAO.createPerson()) - .withMessageMatching( - "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); + .withMessageMatching( + "Node\\(\\d+\\) already exists with label `SimplePerson` and property `name` = '[\\w\\s]+'; Error code 'Neo.ClientError.Schema.ConstraintValidationFailed';.*"); } - interface SimplePersonRepository extends Neo4jRepository {} + interface SimplePersonRepository extends Neo4jRepository { + + } @Repository static class CustomDAO { @@ -130,12 +136,15 @@ class ExceptionTranslationIT { this.neo4jClient = neo4jClient; } - public ResultSummary createPerson() { + ResultSummary createPerson() { - return neo4jClient - .delegateTo(queryRunner -> Optional.of(queryRunner.run("CREATE (:SimplePerson {name: 'Tom'})").consume())) - .run().get(); + return this.neo4jClient + .delegateTo( + queryRunner -> Optional.of(queryRunner.run("CREATE (:SimplePerson {name: 'Tom'})").consume())) + .run() + .get(); } + } @Configuration @@ -146,24 +155,26 @@ class ExceptionTranslationIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public CustomDAO customDAO(Neo4jClient neo4jClient) { + CustomDAO customDAO(Neo4jClient neo4jClient) { return new CustomDAO(neo4jClient); } - // If someone wants to use the plain driver or the delegating mechanism of the client, than they must provide a + // If someone wants to use the plain driver or the delegating mechanism of the + // client, then they must provide a // couple of more beans. @Bean - public Neo4jPersistenceExceptionTranslator neo4jPersistenceExceptionTranslator() { + Neo4jPersistenceExceptionTranslator neo4jPersistenceExceptionTranslator() { return new Neo4jPersistenceExceptionTranslator(); } @Bean - public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() { + PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() { return new PersistenceExceptionTranslationPostProcessor(); } @@ -171,5 +182,7 @@ class ExceptionTranslationIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/IdGeneratorsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/IdGeneratorsIT.java index 9f23bc36c..b126efba7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/IdGeneratorsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/IdGeneratorsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -25,10 +23,10 @@ import java.util.stream.StreamSupport; 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.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -38,10 +36,13 @@ import org.springframework.data.neo4j.integration.shared.common.ThingWithGenerat import org.springframework.data.neo4j.integration.shared.common.ThingWithIdGeneratedByBean; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -77,15 +78,18 @@ class IdGeneratorsIT extends IdGeneratorsITBase { @Test void idGenerationWithNewEntitiesShouldWork(@Autowired ThingWithGeneratedIdRepository repository) { - List things = IntStream.rangeClosed(1, 10).mapToObj(i -> new ThingWithGeneratedId("name" + i)) - .collect(Collectors.toList()); + List things = IntStream.rangeClosed(1, 10) + .mapToObj(i -> new ThingWithGeneratedId("name" + i)) + .collect(Collectors.toList()); Iterable savedThings = repository.saveAll(things); - assertThat(savedThings).hasSize(things.size()).extracting(ThingWithGeneratedId::getTheId) - .allMatch(s -> s.matches("thingWithGeneratedId-\\d+")); + assertThat(savedThings).hasSize(things.size()) + .extracting(ThingWithGeneratedId::getTheId) + .allMatch(s -> s.matches("thingWithGeneratedId-\\d+")); - Set distinctIds = StreamSupport.stream(savedThings.spliterator(), false).map(ThingWithGeneratedId::getTheId) - .collect(Collectors.toSet()); + Set distinctIds = StreamSupport.stream(savedThings.spliterator(), false) + .map(ThingWithGeneratedId::getTheId) + .collect(Collectors.toSet()); assertThat(distinctIds).hasSize(things.size()); } @@ -102,9 +106,13 @@ class IdGeneratorsIT extends IdGeneratorsITBase { verifyDatabase(t.getTheId(), t.getName()); } - interface ThingWithGeneratedIdRepository extends CrudRepository {} + interface ThingWithGeneratedIdRepository extends CrudRepository { - interface ThingWithIdGeneratedByBeanRepository extends CrudRepository {} + } + + interface ThingWithIdGeneratedByBeanRepository extends CrudRepository { + + } @Configuration @EnableTransactionManagement @@ -112,30 +120,35 @@ class IdGeneratorsIT extends IdGeneratorsITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public IdGenerator aFancyIdGenerator() { + IdGenerator aFancyIdGenerator() { return (label, entity) -> "ImperativeID."; } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableAssignedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableAssignedIdsIT.java index e0fe723be..2154e5f7e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableAssignedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableAssignedIdsIT.java @@ -15,15 +15,24 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -38,19 +47,11 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; 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.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -60,6 +61,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class ImmutableAssignedIdsIT { public static final String SOME_VALUE_VALUE = "testValue"; + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; @@ -70,15 +72,14 @@ public class ImmutableAssignedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test // GH-2141 - void saveWithAssignedIdsReturnsObjectWithIdSet( - @Autowired ImmutablePersonWithAssignedIdRepository repository) { + void saveWithAssignedIdsReturnsObjectWithIdSet(@Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId fallback1 = new ImmutablePersonWithAssignedId(); ImmutablePersonWithAssignedId fallback2 = ImmutablePersonWithAssignedId.fallback(fallback1); @@ -95,8 +96,7 @@ public class ImmutableAssignedIdsIT { } @Test // GH-2141 - void saveAllWithAssignedIdsReturnsObjectWithIdSet( - @Autowired ImmutablePersonWithAssignedIdRepository repository) { + void saveAllWithAssignedIdsReturnsObjectWithIdSet(@Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId fallback1 = new ImmutablePersonWithAssignedId(); ImmutablePersonWithAssignedId fallback2 = ImmutablePersonWithAssignedId.fallback(fallback1); @@ -117,7 +117,8 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId onboarder = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .wasOnboardedBy(Collections.singletonList(onboarder)); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -130,7 +131,8 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId knowingPerson = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .knownBy(Collections.singleton(knowingPerson)); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -143,7 +145,8 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId rater = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .ratedBy(Collections.singletonMap("Good", rater)); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -177,7 +180,8 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutableSecondPersonWithAssignedId rater = new ImmutableSecondPersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -189,7 +193,8 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipProperties(properties); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -204,8 +209,10 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -219,78 +226,75 @@ public class ImmutableAssignedIdsIT { @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue) + .isEqualTo(SOME_VALUE_VALUE); } - @Test // GH-2148 void saveRelationshipWithAssignedIdsContainsObjectWithIdSetForRelationshipPropertiesDynamicCollection( @Autowired ImmutablePersonWithAssignedIdRepository repository) { ImmutableSecondPersonWithAssignedId somebody = new ImmutableSecondPersonWithAssignedId(); - ImmutableSecondPersonWithAssignedIdRelationshipProperties properties = new ImmutableSecondPersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithAssignedIdRelationshipProperties properties = new ImmutableSecondPersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } @Test // GH-2148 void saveRelationshipWithAssignedIdsContainsAllRelationshipTypes( @Autowired ImmutablePersonWithAssignedIdRepository repository) { - ImmutablePersonWithAssignedId fallback = - new ImmutablePersonWithAssignedId(); + ImmutablePersonWithAssignedId fallback = new ImmutablePersonWithAssignedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithAssignedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithAssignedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithAssignedId()); + Set knownBy = Collections.singleton(new ImmutablePersonWithAssignedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithAssignedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithAssignedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithAssignedId())); + Map> ratedByCollection = Collections.singletonMap("Na", + Collections.singletonList(new ImmutableSecondPersonWithAssignedId())); - ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithAssignedId()); + ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithAssignedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithAssignedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithAssignedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithAssignedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithAssignedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithAssignedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithAssignedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithAssignedIdRelationshipProperties(null, + "rel4", new ImmutableSecondPersonWithAssignedId()))); - ImmutablePersonWithAssignedId person = new ImmutablePersonWithAssignedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + ImmutablePersonWithAssignedId person = new ImmutablePersonWithAssignedId(null, wasOnboardedBy, knownBy, ratedBy, + ratedByCollection, fallback, relationshipProperties, relationshipPropertiesCollection, + relationshipPropertiesDynamic, relationshipPropertiesDynamicCollection); ImmutablePersonWithAssignedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -316,16 +320,19 @@ public class ImmutableAssignedIdsIT { assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue) + .isEqualTo(SOME_VALUE_VALUE); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } @Test // GH-2235 - void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode(@Autowired ImmutablePersonWithAssignedIdRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode( + @Autowired ImmutablePersonWithAssignedIdRepository repository, @Autowired BookmarkCapture bookmarkCapture) { ImmutablePersonWithAssignedId person1 = new ImmutablePersonWithAssignedId(); ImmutablePersonWithAssignedId person2 = ImmutablePersonWithAssignedId.fallback(person1); List onboardedBy = new ArrayList<>(); @@ -337,24 +344,27 @@ public class ImmutableAssignedIdsIT { assertThat(savedPerson.id).isNotNull(); assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithAssignedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null) - .findFirst().get(); + ImmutablePersonWithAssignedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); assertThat(savedPerson2.fallback.id).isNotNull(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithAssignedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithAssignedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithAssignedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + List result = session + .run("MATCH (person3:ImmutablePersonWithAssignedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithAssignedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithAssignedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } - interface ImmutablePersonWithAssignedIdRepository extends Neo4jRepository {} + interface ImmutablePersonWithAssignedIdRepository extends Neo4jRepository { + + } @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @@ -362,6 +372,7 @@ public class ImmutableAssignedIdsIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -380,7 +391,9 @@ public class ImmutableAssignedIdsIT { } @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -390,20 +403,24 @@ public class ImmutableAssignedIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableExternallyGeneratedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableExternallyGeneratedIdsIT.java index 366caf04a..3b0bc6bfd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableExternallyGeneratedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableExternallyGeneratedIdsIT.java @@ -15,15 +15,25 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -37,20 +47,11 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; 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.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -69,7 +70,7 @@ public class ImmutableExternallyGeneratedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -80,7 +81,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId fallback1 = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId.fallback(fallback1); + ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId + .fallback(fallback1); ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.fallback(fallback2); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.save(person); @@ -95,7 +97,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId fallback1 = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId.fallback(fallback1); + ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId + .fallback(fallback1); ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.fallback(fallback2); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -110,7 +113,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId onboarder = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .wasOnboardedBy(Collections.singletonList(onboarder)); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -122,7 +126,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId knowingPerson = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .knownBy(Collections.singleton(knowingPerson)); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -134,7 +139,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId rater = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .ratedBy(Collections.singletonMap("Good", rater)); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -165,7 +171,8 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutableSecondPersonWithExternallyGeneratedId rater = new ImmutableSecondPersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -177,8 +184,10 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipProperties(properties); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipProperties(properties); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -191,8 +200,10 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -205,8 +216,10 @@ public class ImmutableExternallyGeneratedIdsIT { @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -215,67 +228,64 @@ public class ImmutableExternallyGeneratedIdsIT { assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); } - @Test // GH-2148 void saveRelationshipWithExternallyGeneratedIdsContainsObjectWithIdSetForRelationshipPropertiesDynamicCollection( @Autowired ImmutablePersonWithExternalIdRepository repository) { ImmutableSecondPersonWithExternallyGeneratedId somebody = new ImmutableSecondPersonWithExternallyGeneratedId(); - ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } @Test // GH-2148 void saveRelationshipWithExternallyGeneratedIdsContainsAllRelationshipTypes( @Autowired ImmutablePersonWithExternalIdRepository repository) { - ImmutablePersonWithExternallyGeneratedId fallback = - new ImmutablePersonWithExternallyGeneratedId(); + ImmutablePersonWithExternallyGeneratedId fallback = new ImmutablePersonWithExternallyGeneratedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithExternallyGeneratedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithExternallyGeneratedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithExternallyGeneratedId()); + Set knownBy = Collections + .singleton(new ImmutablePersonWithExternallyGeneratedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithExternallyGeneratedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithExternallyGeneratedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedId())); + Map> ratedByCollection = Collections + .singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedId())); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithExternallyGeneratedId()); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithExternallyGeneratedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithExternallyGeneratedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithExternallyGeneratedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithExternallyGeneratedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithExternallyGeneratedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithExternallyGeneratedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( + null, "rel4", new ImmutableSecondPersonWithExternallyGeneratedId()))); ImmutablePersonWithExternallyGeneratedId person = new ImmutablePersonWithExternallyGeneratedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + wasOnboardedBy, knownBy, ratedBy, ratedByCollection, fallback, relationshipProperties, + relationshipPropertiesCollection, relationshipPropertiesDynamic, + relationshipPropertiesDynamicCollection); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.saveAll(Collections.singleton(person)).get(0); @@ -301,42 +311,49 @@ public class ImmutableExternallyGeneratedIdsIT { assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } @Test // GH-2235 - void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode(@Autowired ImmutablePersonWithExternalIdRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode( + @Autowired ImmutablePersonWithExternalIdRepository repository, @Autowired BookmarkCapture bookmarkCapture) { ImmutablePersonWithExternallyGeneratedId person1 = new ImmutablePersonWithExternallyGeneratedId(); ImmutablePersonWithExternallyGeneratedId person2 = ImmutablePersonWithExternallyGeneratedId.fallback(person1); List onboardedBy = new ArrayList<>(); onboardedBy.add(person1); onboardedBy.add(person2); - ImmutablePersonWithExternallyGeneratedId person3 = ImmutablePersonWithExternallyGeneratedId.wasOnboardedBy(onboardedBy); + ImmutablePersonWithExternallyGeneratedId person3 = ImmutablePersonWithExternallyGeneratedId + .wasOnboardedBy(onboardedBy); ImmutablePersonWithExternallyGeneratedId savedPerson = repository.save(person3); assertThat(savedPerson.id).isNotNull(); assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithExternallyGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null) - .findFirst().get(); + ImmutablePersonWithExternallyGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); assertThat(savedPerson2.fallback.id).isNotNull(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithExternallyGeneratedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithExternallyGeneratedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithExternallyGeneratedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + List result = session + .run("MATCH (person3:ImmutablePersonWithExternallyGeneratedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithExternallyGeneratedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithExternallyGeneratedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } - interface ImmutablePersonWithExternalIdRepository extends Neo4jRepository {} + interface ImmutablePersonWithExternalIdRepository + extends Neo4jRepository { + + } @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @@ -344,6 +361,7 @@ public class ImmutableExternallyGeneratedIdsIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -354,7 +372,9 @@ public class ImmutableExternallyGeneratedIdsIT { } @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -364,20 +384,24 @@ public class ImmutableExternallyGeneratedIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableGeneratedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableGeneratedIdsIT.java index aae870799..592d6f49c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableGeneratedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ImmutableGeneratedIdsIT.java @@ -15,15 +15,24 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.convert.Neo4jConversions; @@ -40,19 +49,11 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; 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.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -71,15 +72,14 @@ public class ImmutableGeneratedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test // GH-2141 - void saveWithGeneratedIdsReturnsObjectWithIdSet( - @Autowired ImmutablePersonWithGeneratedIdRepository repository) { + void saveWithGeneratedIdsReturnsObjectWithIdSet(@Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId fallback1 = new ImmutablePersonWithGeneratedId(); ImmutablePersonWithGeneratedId fallback2 = ImmutablePersonWithGeneratedId.fallback(fallback1); @@ -97,7 +97,8 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId onboarder = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .wasOnboardedBy(Collections.singletonList(onboarder)); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -110,7 +111,8 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId knowingPerson = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .knownBy(Collections.singleton(knowingPerson)); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -123,7 +125,8 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId rater = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .ratedBy(Collections.singletonMap("Good", rater)); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -156,7 +159,8 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutableSecondPersonWithGeneratedId rater = new ImmutableSecondPersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -169,7 +173,8 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipProperties(properties); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -184,8 +189,10 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -199,8 +206,10 @@ public class ImmutableGeneratedIdsIT { @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -210,68 +219,62 @@ public class ImmutableGeneratedIdsIT { assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); } - @Test // GH-2148 void saveRelationshipWithGeneratedIdsContainsObjectWithIdSetForRelationshipPropertiesDynamicCollection( @Autowired ImmutablePersonWithGeneratedIdRepository repository) { ImmutableSecondPersonWithGeneratedId somebody = new ImmutableSecondPersonWithGeneratedId(); - ImmutableSecondPersonWithGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); assertThat(person.id).isNull(); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } @Test // GH-2148 void saveRelationshipWithGeneratedIdsContainsAllRelationshipTypes( @Autowired ImmutablePersonWithGeneratedIdRepository repository) { - ImmutablePersonWithGeneratedId fallback = - new ImmutablePersonWithGeneratedId(); + ImmutablePersonWithGeneratedId fallback = new ImmutablePersonWithGeneratedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithGeneratedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithGeneratedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithGeneratedId()); + Set knownBy = Collections.singleton(new ImmutablePersonWithGeneratedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithGeneratedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithGeneratedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithGeneratedId())); + Map> ratedByCollection = Collections.singletonMap("Na", + Collections.singletonList(new ImmutableSecondPersonWithGeneratedId())); - ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithGeneratedId()); + ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithGeneratedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithGeneratedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithGeneratedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithGeneratedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithGeneratedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithGeneratedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithGeneratedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithGeneratedIdRelationshipProperties(null, + "rel4", new ImmutableSecondPersonWithGeneratedId()))); - ImmutablePersonWithGeneratedId person = new ImmutablePersonWithGeneratedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + ImmutablePersonWithGeneratedId person = new ImmutablePersonWithGeneratedId(null, wasOnboardedBy, knownBy, + ratedBy, ratedByCollection, fallback, relationshipProperties, relationshipPropertiesCollection, + relationshipPropertiesDynamic, relationshipPropertiesDynamicCollection); ImmutablePersonWithGeneratedId savedPerson = repository.save(person); @@ -298,12 +301,12 @@ public class ImmutableGeneratedIdsIT { assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); } - interface ImmutablePersonWithGeneratedIdRepository extends Neo4jRepository {} - @Test // GH-2148 void childrenShouldNotBeRecreatedForNoReasons(@Autowired Neo4jTemplate template) { @@ -334,27 +337,34 @@ public class ImmutableGeneratedIdsIT { assertThat(savedPerson.id).isNotNull(); assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null).findFirst().get(); + ImmutablePersonWithGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); assertThat(savedPerson2.fallback.id).isNotNull(); - try (Session session = driver.session()) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithGeneratedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithGeneratedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithGeneratedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session()) { + List result = session + .run("MATCH (person3:ImmutablePersonWithGeneratedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithGeneratedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithGeneratedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } + interface ImmutablePersonWithGeneratedIdRepository extends Neo4jRepository { + + } + @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @EnableTransactionManagement static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -365,7 +375,9 @@ public class ImmutableGeneratedIdsIT { } @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -375,20 +387,24 @@ public class ImmutableGeneratedIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java index 9fd142aa5..3eb332076 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.List; @@ -32,6 +30,7 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.types.Node; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -58,6 +57,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier * @author Michael J. Simons @@ -74,7 +75,8 @@ public class InheritanceMappingIT { private final TransactionTemplate transactionTemplate; @Autowired - public InheritanceMappingIT(Driver driver, BookmarkCapture bookmarkCapture, TransactionTemplate transactionTemplate) { + public InheritanceMappingIT(Driver driver, BookmarkCapture bookmarkCapture, + TransactionTemplate transactionTemplate) { this.driver = driver; this.bookmarkCapture = bookmarkCapture; this.transactionTemplate = transactionTemplate; @@ -82,9 +84,9 @@ public class InheritanceMappingIT { @BeforeEach void deleteData() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -92,10 +94,13 @@ public class InheritanceMappingIT { void relationshipsShouldHaveCorrectTypes(@Autowired BuildingRepository repository) { Long buildingId; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - buildingId = session.run("CREATE (b:Building:Entity{name:'b'})-[:IS_CHILD]->(:Site:Entity{name:'s'})-[:IS_CHILD]->(:Company:Entity{name:'c'}) return id(b) as id").single() - .get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + buildingId = session.run( + "CREATE (b:Building:Entity{name:'b'})-[:IS_CHILD]->(:Site:Entity{name:'s'})-[:IS_CHILD]->(:Company:Entity{name:'c'}) return id(b) as id") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } Inheritance.Building building = repository.findById(buildingId).get(); @@ -120,11 +125,8 @@ public class InheritanceMappingIT { Inheritance.Continent continent = new Inheritance.Continent("continent", "small"); Inheritance.GenericTerritory genericTerritory = new Inheritance.GenericTerritory("generic"); - assertThat(((Inheritance.Country) territory).relationshipList).containsExactlyInAnyOrder( - country, - continent, - genericTerritory - ); + assertThat(((Inheritance.Country) territory).relationshipList).containsExactlyInAnyOrder(country, continent, + genericTerritory); } @Test // GH-2138 @@ -139,49 +141,50 @@ public class InheritanceMappingIT { Inheritance.Continent continent = new Inheritance.Continent("continent", "small"); Inheritance.GenericTerritory genericTerritory = new Inheritance.GenericTerritory("generic"); - assertThat(territories).containsExactlyInAnyOrder( - country1, - country2, - continent, - genericTerritory - ); + assertThat(territories).containsExactlyInAnyOrder(country1, country2, continent, genericTerritory); } @Test // GH-2199 void findAndMapAllConcreteSubclassesWithoutParentLabel(@Autowired PetsRepository petsRepository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { transaction.run("CREATE (:Cat{name:'a'})"); transaction.run("CREATE (:Cat{name:'a'})"); transaction.run("CREATE (:Cat{name:'a'})"); transaction.run("CREATE (:Dog{name:'a'})"); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } List pets = petsRepository.findPets("a"); - assertThat(pets) - .hasOnlyElementsOfType(AbstractPet.class) - .hasAtLeastOneElementOfType(Dog.class) - .hasAtLeastOneElementOfType(Cat.class); + assertThat(pets).hasOnlyElementsOfType(AbstractPet.class) + .hasAtLeastOneElementOfType(Dog.class) + .hasAtLeastOneElementOfType(Cat.class); } @Test // GH-2201 void shouldDealWithInterfacesWithoutNodeAnnotationRead(@Autowired Neo4jTemplate template) { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - id = transaction.run("CREATE (s:SomeInterface{name:'s'}) -[:RELATED]-> (:SomeInterface {name:'e'}) RETURN id(s)").single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction + .run("CREATE (s:SomeInterface{name:'s'}) -[:RELATED]-> (:SomeInterface {name:'e'}) RETURN id(s)") + .single() + .get(0) + .asLong(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional optionalEntity = template.findById(id, Inheritance.SomeInterfaceEntity.class); + Optional optionalEntity = template.findById(id, + Inheritance.SomeInterfaceEntity.class); assertThat(optionalEntity).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.SomeInterface::getRelated) - .extracting(Inheritance.SomeInterface::getName).isEqualTo("e"); + .extracting(Inheritance.SomeInterface::getName) + .isEqualTo("e"); }); } @@ -192,11 +195,13 @@ public class InheritanceMappingIT { entity.setRelated(new Inheritance.SomeInterfaceEntity("e")); long id = template.save(entity).getId(); - Optional optionalEntity = template.findById(id, Inheritance.SomeInterfaceEntity.class); + Optional optionalEntity = template.findById(id, + Inheritance.SomeInterfaceEntity.class); assertThat(optionalEntity).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.SomeInterface::getRelated) - .extracting(Inheritance.SomeInterface::getName).isEqualTo("e"); + .extracting(Inheritance.SomeInterface::getName) + .isEqualTo("e"); }); } @@ -204,17 +209,24 @@ public class InheritanceMappingIT { void shouldDealWithInterfacesWithNodeAnnotationRead(@Autowired Neo4jTemplate template) { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - id = transaction.run("CREATE (s:PrimaryLabelWN{name:'s'}) -[:RELATED]-> (:PrimaryLabelWN {name:'e'}) RETURN id(s)").single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction + .run("CREATE (s:PrimaryLabelWN{name:'s'}) -[:RELATED]-> (:PrimaryLabelWN {name:'e'}) RETURN id(s)") + .single() + .get(0) + .asLong(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional optionalEntity = transactionTemplate.execute(tx -> template.findById(id, Inheritance.SomeInterfaceEntity2.class)); + Optional optionalEntity = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.SomeInterfaceEntity2.class)); assertThat(optionalEntity).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.SomeInterface2::getRelated) - .extracting(Inheritance.SomeInterface2::getName).isEqualTo("e"); + .extracting(Inheritance.SomeInterface2::getName) + .isEqualTo("e"); }); } @@ -223,13 +235,15 @@ public class InheritanceMappingIT { Inheritance.SomeInterfaceEntity2 entity = new Inheritance.SomeInterfaceEntity2("s"); entity.setRelated(new Inheritance.SomeInterfaceEntity2("e")); - long id = transactionTemplate.execute(tx -> template.save(entity).getId()); + long id = this.transactionTemplate.execute(tx -> template.save(entity).getId()); - Optional optionalEntity = transactionTemplate.execute(tx -> template.findById(id, Inheritance.SomeInterfaceEntity2.class)); + Optional optionalEntity = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.SomeInterfaceEntity2.class)); assertThat(optionalEntity).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.SomeInterface2::getRelated) - .extracting(Inheritance.SomeInterface2::getName).isEqualTo("e"); + .extracting(Inheritance.SomeInterface2::getName) + .isEqualTo("e"); }); } @@ -237,58 +251,61 @@ public class InheritanceMappingIT { void complexInterfaceMapping(@Autowired Neo4jTemplate template) { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - id = transaction.run("" + - "CREATE (s:SomeInterface3:SomeInterface3a{name:'s'}) " + - "-[:RELATED]-> (:SomeInterface3:SomeInterface3b {name:'m'}) " + - "-[:RELATED]-> (:SomeInterface3:SomeInterface3a {name:'e'}) RETURN id(s)") - .single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction + .run("" + "CREATE (s:SomeInterface3:SomeInterface3a{name:'s'}) " + + "-[:RELATED]-> (:SomeInterface3:SomeInterface3b {name:'m'}) " + + "-[:RELATED]-> (:SomeInterface3:SomeInterface3a {name:'e'}) RETURN id(s)") + .single() + .get(0) + .asLong(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional optionalEntity = transactionTemplate.execute(tx -> template.findById(id, Inheritance.SomeInterfaceImpl3a.class)); + Optional optionalEntity = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.SomeInterfaceImpl3a.class)); assertThat(optionalEntity).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.SomeInterface3::getRelated) - .extracting(Inheritance.SomeInterface3::getName).isEqualTo("m"); + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("m"); assertThat(v).extracting(Inheritance.SomeInterface3::getRelated) - .extracting(Inheritance.SomeInterface3::getRelated) - .extracting(Inheritance.SomeInterface3::getName).isEqualTo("e"); + .extracting(Inheritance.SomeInterface3::getRelated) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("e"); }); } @Test // GH-2201 void mixedImplementationsRead(@Autowired Neo4jTemplate template) { - // tag::interface3[] Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - id = transaction.run("" + - "CREATE (s:ParentModel{name:'s'}) " + - "CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) " + - "CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) " + - "RETURN id(s)") - .single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction.run(""" + CREATE (s:ParentModel{name:'s'}) + CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) + CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) + RETURN id(s)""").single().get(0).asLong(); transaction.commit(); - // end::interface3[] - bookmarkCapture.seedWith(session.lastBookmarks()); - // tag::interface3[] + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional optionalParentModel = transactionTemplate.execute(tx -> - template.findById(id, Inheritance.ParentModel.class)); + Optional optionalParentModel = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.ParentModel.class)); assertThat(optionalParentModel).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("s"); assertThat(v).extracting(Inheritance.ParentModel::getRelated1) - .isInstanceOf(Inheritance.SomeInterfaceImpl3b.class) - .extracting(Inheritance.SomeInterface3::getName) - .isEqualTo("3b"); + .isInstanceOf(Inheritance.SomeInterfaceImpl3b.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("3b"); assertThat(v).extracting(Inheritance.ParentModel::getRelated2) - .isInstanceOf(Inheritance.SomeInterfaceImpl3a.class) - .extracting(Inheritance.SomeInterface3::getName) - .isEqualTo("3a"); + .isInstanceOf(Inheritance.SomeInterfaceImpl3a.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("3a"); }); // end::interface3[] } @@ -300,31 +317,33 @@ public class InheritanceMappingIT { entity.setRelated1(new Inheritance.SomeInterfaceImpl3b("r13b")); entity.setRelated2(new Inheritance.SomeInterfaceImpl3a("r13a")); - long id = transactionTemplate.execute(tx -> template.save(entity).getId()); + long id = this.transactionTemplate.execute(tx -> template.save(entity).getId()); - Optional optionalParentModel = transactionTemplate.execute(tx -> template.findById(id, Inheritance.ParentModel.class)); + Optional optionalParentModel = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.ParentModel.class)); assertThat(optionalParentModel).hasValueSatisfying(v -> { assertThat(v.getName()).isEqualTo("d"); assertThat(v).extracting(Inheritance.ParentModel::getRelated1) - .isInstanceOf(Inheritance.SomeInterfaceImpl3b.class) - .extracting(Inheritance.SomeInterface3::getName) - .isEqualTo("r13b"); + .isInstanceOf(Inheritance.SomeInterfaceImpl3b.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("r13b"); assertThat(v).extracting(Inheritance.ParentModel::getRelated2) - .isInstanceOf(Inheritance.SomeInterfaceImpl3a.class) - .extracting(Inheritance.SomeInterface3::getName) - .isEqualTo("r13a"); + .isInstanceOf(Inheritance.SomeInterfaceImpl3a.class) + .extracting(Inheritance.SomeInterface3::getName) + .isEqualTo("r13a"); }); } @Test // GH-2201 void mixedInterfaces(@Autowired Neo4jTemplate template) { - Inheritance.Mix1AndMix2 mix1AndMix2 = transactionTemplate.execute(tx -> template.save(new Inheritance.Mix1AndMix2("a", "b"))); + Inheritance.Mix1AndMix2 mix1AndMix2 = this.transactionTemplate + .execute(tx -> template.save(new Inheritance.Mix1AndMix2("a", "b"))); assertThat(mix1AndMix2.getName()).isEqualTo("a"); assertThat(mix1AndMix2.getValue()).isEqualTo("b"); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { List records = session.run("MATCH (n) RETURN n").list(); assertThat(records).hasSize(1); Record record = records.get(0); @@ -336,35 +355,36 @@ public class InheritanceMappingIT { @Test // GH-2788 void detectPropertiesAndRelationshipsOfImplementingEntities(@Autowired Neo4jTemplate template) { String id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - id = transaction.run("" + - "CREATE (e:`GH-2788-Entity`) " + - "CREATE (e)-[:RELATED_TO]-> (a:`GH-2788-Interface`:`GH-2788-A` {name:'A'}) " + - "CREATE (e)-[:RELATED_TO]-> (b:`GH-2788-Interface`:`GH-2788-B` {name:'B'}) " + - "CREATE (a)-[:RELATED_TO]-> (:`Gh2788ArelatedEntity`) " + - "CREATE (b)-[:RELATED_TO]-> (:`Gh2788BrelatedEntity`) " + - "RETURN elementId(e)") - .single().get(0).asString(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + id = transaction + .run("" + "CREATE (e:`GH-2788-Entity`) " + + "CREATE (e)-[:RELATED_TO]-> (a:`GH-2788-Interface`:`GH-2788-A` {name:'A'}) " + + "CREATE (e)-[:RELATED_TO]-> (b:`GH-2788-Interface`:`GH-2788-B` {name:'B'}) " + + "CREATE (a)-[:RELATED_TO]-> (:`Gh2788ArelatedEntity`) " + + "CREATE (b)-[:RELATED_TO]-> (:`Gh2788BrelatedEntity`) " + "RETURN elementId(e)") + .single() + .get(0) + .asString(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional gh2788Entity = transactionTemplate.execute(tx -> - template.findById(id, Inheritance.Gh2788Entity.class)); + Optional gh2788Entity = this.transactionTemplate + .execute(tx -> template.findById(id, Inheritance.Gh2788Entity.class)); assertThat(gh2788Entity).hasValueSatisfying(v -> { List relatedTo = v.relatedTo; assertThat(relatedTo).allSatisfy(relatedElement -> { if (relatedElement instanceof Inheritance.Gh2788A relatedAelement) { assertThat(relatedAelement.name).isEqualTo("A"); - assertThat(relatedAelement.relatedTo) - .hasSize(1) - .hasOnlyElementsOfType(Inheritance.Gh2788ArelatedEntity.class); - } else if (relatedElement instanceof Inheritance.Gh2788B relatedBelement) { + assertThat(relatedAelement.relatedTo).hasSize(1) + .hasOnlyElementsOfType(Inheritance.Gh2788ArelatedEntity.class); + } + else if (relatedElement instanceof Inheritance.Gh2788B relatedBelement) { assertThat(relatedBelement.name).isEqualTo("B"); - assertThat(relatedBelement.relatedTo) - .hasSize(1) - .hasOnlyElementsOfType(Inheritance.Gh2788BrelatedEntity.class); + assertThat(relatedBelement.relatedTo).hasSize(1) + .hasOnlyElementsOfType(Inheritance.Gh2788BrelatedEntity.class); } }); @@ -376,7 +396,8 @@ public class InheritanceMappingIT { Record divisionAndTerritoryId = createDivisionAndTerritories(); - Optional optionalDivision = repository.findById(divisionAndTerritoryId.get("divisionId").asLong()); + Optional optionalDivision = repository + .findById(divisionAndTerritoryId.get("divisionId").asLong()); assertThat(optionalDivision).isPresent(); assertThat(optionalDivision).hasValueSatisfying(twoDifferentClassesHaveBeenLoaded()); } @@ -395,9 +416,10 @@ public class InheritanceMappingIT { return d -> { assertThat(d.getIsActiveIn()).hasSize(2); assertThat(d.getIsActiveIn()).extracting(Inheritance.BaseTerritory::getNameEn) - .containsExactlyInAnyOrder("anotherCountry", "continent"); - Map classByName = d.getIsActiveIn().stream() - .collect(Collectors.toMap(Inheritance.BaseTerritory::getNameEn, v -> v.getClass())); + .containsExactlyInAnyOrder("anotherCountry", "continent"); + Map classByName = d.getIsActiveIn() + .stream() + .collect(Collectors.toMap(Inheritance.BaseTerritory::getNameEn, v -> v.getClass())); assertThat(classByName).containsEntry("anotherCountry", Inheritance.Country.class); assertThat(classByName).containsEntry("continent", Inheritance.Continent.class); }; @@ -427,20 +449,25 @@ public class InheritanceMappingIT { @Test // GH-2262 void shouldMatchPolymorphicKotlinInterfacesWhenFetchingAll(@Autowired CinemaRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - transaction.run("CREATE (:KotlinMovie:KotlinAnimationMovie {id: 'movie001', name: 'movie-001', studio: 'Pixar'})<-[:Plays]-(c:KotlinCinema {id:'cine-01', name: 'GrandRex'}) RETURN id(c) AS id") - .single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + transaction.run( + "CREATE (:KotlinMovie:KotlinAnimationMovie {id: 'movie001', name: 'movie-001', studio: 'Pixar'})<-[:Plays]-(c:KotlinCinema {id:'cine-01', name: 'GrandRex'}) RETURN id(c) AS id") + .single() + .get(0) + .asLong(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } List divisions = repository.findAll(); assertThat(divisions).hasSize(1); assertThat(divisions).first().satisfies(c -> { assertThat(c.getPlays()).hasSize(1); - assertThat(c.getPlays()).first().isInstanceOf(KotlinAnimationMovie.class) - .extracting(m -> ((KotlinAnimationMovie) m).getStudio()) - .isEqualTo("Pixar"); + assertThat(c.getPlays()).first() + .isInstanceOf(KotlinAnimationMovie.class) + .extracting(m -> ((KotlinAnimationMovie) m).getStudio()) + .isEqualTo("Pixar"); }); } @@ -448,11 +475,13 @@ public class InheritanceMappingIT { void loadAndPopulateRelationshipFromTheHierarchy(@Autowired ParentClassWithRelationshipRepository repository) { long childId; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - childId = session - .run("CREATE (c:CCWR:PCWR{name:'child'})-[:LIVES_IN]->(:Continent:BaseTerritory:BaseEntity{nameEn:'continent', continentProperty:'small'}) return id(c) as id") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + childId = session.run( + "CREATE (c:CCWR:PCWR{name:'child'})-[:LIVES_IN]->(:Continent:BaseTerritory:BaseEntity{nameEn:'continent', continentProperty:'small'}) return id(c) as id") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } Inheritance.ParentClassWithRelationship potentialChildClass = repository.findById(childId).get(); @@ -469,9 +498,10 @@ public class InheritanceMappingIT { return d -> { assertThat(d.getIsRelatedTo()).hasSize(2); assertThat(d.getIsRelatedTo()).extracting(Inheritance.SomeInterface3::getName) - .containsExactlyInAnyOrder("3a", "3b"); - Map classByName = d.getIsRelatedTo().stream() - .collect(Collectors.toMap(Inheritance.SomeInterface3::getName, v -> v.getClass())); + .containsExactlyInAnyOrder("3a", "3b"); + Map classByName = d.getIsRelatedTo() + .stream() + .collect(Collectors.toMap(Inheritance.SomeInterface3::getName, v -> v.getClass())); assertThat(classByName).containsEntry("3a", Inheritance.SomeInterfaceImpl3a.class); assertThat(classByName).containsEntry("3b", Inheritance.SomeInterfaceImpl3b.class); }; @@ -479,30 +509,31 @@ public class InheritanceMappingIT { private Record createDivisionAndTerritories() { Record result; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { - result = session.run("CREATE (c:Country:BaseTerritory:BaseEntity{nameEn:'country', countryProperty:'baseCountry'}) " + - "CREATE (c)-[:LINK]->(ca:Country:BaseTerritory:BaseEntity{nameEn:'anotherCountry', countryProperty:'large'}) " + - "CREATE (c)-[:LINK]->(cb:Continent:BaseTerritory:BaseEntity{nameEn:'continent', continentProperty:'small'}) " + - "CREATE (c)-[:LINK]->(:GenericTerritory:BaseTerritory:BaseEntity{nameEn:'generic'}) " + - "CREATE (d:Division:BaseEntity{name:'Division'}) " + - "CREATE (d) -[:IS_ACTIVE_IN] -> (ca)" + - "CREATE (d) -[:IS_ACTIVE_IN] -> (cb)" + - "RETURN id(d) as divisionId, id(c) as territoryId").single(); - bookmarkCapture.seedWith(session.lastBookmarks()); + result = session + .run("CREATE (c:Country:BaseTerritory:BaseEntity{nameEn:'country', countryProperty:'baseCountry'}) " + + "CREATE (c)-[:LINK]->(ca:Country:BaseTerritory:BaseEntity{nameEn:'anotherCountry', countryProperty:'large'}) " + + "CREATE (c)-[:LINK]->(cb:Continent:BaseTerritory:BaseEntity{nameEn:'continent', continentProperty:'small'}) " + + "CREATE (c)-[:LINK]->(:GenericTerritory:BaseTerritory:BaseEntity{nameEn:'generic'}) " + + "CREATE (d:Division:BaseEntity{name:'Division'}) " + "CREATE (d) -[:IS_ACTIVE_IN] -> (ca)" + + "CREATE (d) -[:IS_ACTIVE_IN] -> (cb)" + "RETURN id(d) as divisionId, id(c) as territoryId") + .single(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } return result; } private Record createRelationsToDifferentImplementations() { Record result; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { - result = session.run("CREATE (p:ParentModel2) " + - "CREATE (p)-[:IS_RELATED_TO]->(:SomeInterface3:SomeInterface3a {name: '3a'}) " + - "CREATE (p)-[:IS_RELATED_TO]->(:SomeInterface3:SomeInterface3b {name: '3b'}) " + - "RETURN p").single(); - bookmarkCapture.seedWith(session.lastBookmarks()); + result = session + .run("CREATE (p:ParentModel2) " + + "CREATE (p)-[:IS_RELATED_TO]->(:SomeInterface3:SomeInterface3a {name: '3a'}) " + + "CREATE (p)-[:IS_RELATED_TO]->(:SomeInterface3:SomeInterface3b {name: '3b'}) " + "RETURN p") + .single(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } return result; } @@ -515,18 +546,30 @@ public class InheritanceMappingIT { } - interface ParentClassWithRelationshipRepository extends Neo4jRepository { + interface ParentClassWithRelationshipRepository + extends Neo4jRepository { + } - interface BuildingRepository extends Neo4jRepository {} + interface BuildingRepository extends Neo4jRepository { - interface TerritoryRepository extends Neo4jRepository {} + } - interface DivisionRepository extends Neo4jRepository {} + interface TerritoryRepository extends Neo4jRepository { - interface ParentModelRepository extends Neo4jRepository {} + } - interface CinemaRepository extends Neo4jRepository {} + interface DivisionRepository extends Neo4jRepository { + + } + + interface ParentModelRepository extends Neo4jRepository { + + } + + interface CinemaRepository extends Neo4jRepository { + + } @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @@ -539,25 +582,28 @@ public class InheritanceMappingIT { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Bean @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } @@ -565,5 +611,7 @@ public class InheritanceMappingIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jClientIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jClientIT.java index 7b44d0344..b57899cd2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jClientIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jClientIT.java @@ -15,6 +15,10 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.cypherdsl.core.Cypher; @@ -24,6 +28,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,10 +46,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -58,10 +59,8 @@ class Neo4jClientIT { @BeforeEach void setupData(@Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { - try ( - Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.commit(); } @@ -69,28 +68,31 @@ class Neo4jClientIT { @Test // GH-2238 void clientShouldIntegrateWithCypherDSL(@Autowired TransactionTemplate transactionTemplate, - @Autowired Neo4jClient client, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired Neo4jClient client, @Autowired BookmarkCapture bookmarkCapture) { - Node namedAnswer = Cypher.node("TheAnswer", Cypher.mapOf("value", - Cypher.literalOf(23).multiply(Cypher.literalOf(2)).subtract(Cypher.literalOf(4)))).named("n"); - Statement statement = Cypher.create(namedAnswer) - .returning(namedAnswer) - .build(); + Node namedAnswer = Cypher + .node("TheAnswer", + Cypher.mapOf("value", + Cypher.literalOf(23).multiply(Cypher.literalOf(2)).subtract(Cypher.literalOf(4)))) + .named("n"); + Statement statement = Cypher.create(namedAnswer).returning(namedAnswer).build(); Long vanishedId = transactionTemplate.execute(transactionStatus -> { List records = client.getQueryRunner().run(statement.getCypher()).list(); - assertThat(records).hasSize(1) - .first().extracting(r -> r.get("n").get("value").asLong()).isEqualTo(42L); + assertThat(records).hasSize(1).first().extracting(r -> r.get("n").get("value").asLong()).isEqualTo(42L); transactionStatus.setRollbackOnly(); return TestIdentitySupport.getInternalId(records.get(0).get("n").asNode()); }); - // Make sure we actually interacted with the managed transaction (that had been rolled back) + // Make sure we actually interacted with the managed transaction (that had been + // rolled back) try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run("MATCH (n) WHERE id(n) = $id RETURN count(n)", - Collections.singletonMap("id", vanishedId)).single().get(0).asLong(); + long cnt = session + .run("MATCH (n) WHERE id(n) = $id RETURN count(n)", Collections.singletonMap("id", vanishedId)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(0L); } } @@ -105,13 +107,14 @@ class Neo4jClientIT { return neo4jConnectionSupport.getDriver(); } - @Override // needed here because there is no implicit registration of entities upfront some methods under test + @Override // needed here because there is no implicit registration of entities + // upfront some methods under test protected Collection getMappingBasePackages() { return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -125,7 +128,7 @@ class Neo4jClientIT { } @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } @@ -133,5 +136,7 @@ class Neo4jClientIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java index 5464baf91..33d2a93f0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java @@ -15,6 +15,18 @@ */ package org.springframework.data.neo4j.integration.imperative; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.cypherdsl.core.Cypher; @@ -27,6 +39,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,17 +63,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiPredicate; -import java.util.function.Function; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -73,16 +75,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ @Neo4jIntegrationTest class Neo4jTemplateIT { + private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; protected static Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; + private final Neo4jTemplate neo4jTemplate; + private final BookmarkCapture bookmarkCapture; private Long person1Id; + private Long person2Id; @Autowired @@ -92,38 +99,54 @@ class Neo4jTemplateIT { this.bookmarkCapture = bookmarkCapture; } + private static BiPredicate create2LevelProjectingPredicate() { + BiPredicate predicate = (path, property) -> false; + predicate = predicate.or((path, property) -> property.getName().equals("lastName")); + predicate = predicate.or((path, property) -> property.getName().equals("address") + || path.toDotPath().startsWith("address.") && property.getName().equals("street")); + predicate = predicate.or((path, property) -> property.getName().equals("country") + || path.toDotPath().contains("address.country.") && property.getName().equals("name")); + return predicate; + } + @BeforeEach void setupData() { - try ( - Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); - person1Id = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", - Values.parameters("name", TEST_PERSON1_NAME)).single().get("id").asLong(); - person2Id = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", - Values.parameters("name", TEST_PERSON2_NAME)).single().get("id").asLong(); + this.person1Id = transaction + .run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", + Values.parameters("name", TEST_PERSON1_NAME)) + .single() + .get("id") + .asLong(); + this.person2Id = transaction + .run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", + Values.parameters("name", TEST_PERSON2_NAME)) + .single() + .get("id") + .asLong(); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); - transaction.run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" + - " -[:LIVES_AT]-> (a:Address {city: 'Aachen', id: 1})" + - " -[:BASED_IN]->(c:YetAnotherCountryEntity{name: 'Gemany', countryCode: 'DE'})" + - " RETURN id(p)"); + transaction.run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" + + " -[:LIVES_AT]-> (a:Address {city: 'Aachen', id: 1})" + + " -[:BASED_IN]->(c:YetAnotherCountryEntity{name: 'Gemany', countryCode: 'DE'})" + + " RETURN id(p)"); transaction.run( "CREATE (p:Person{firstName: 'Helge', lastName: 'Schnitzel'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'}) RETURN id(p)"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.run("CREATE (p:PersonWithAssignedId{id: 'x', firstName: 'John', lastName: 'Doe'})"); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @Test void count() { - assertThat(neo4jTemplate.count(PersonWithAllConstructor.class)).isEqualTo(2); + assertThat(this.neo4jTemplate.count(PersonWithAllConstructor.class)).isEqualTo(2); } @Test @@ -131,35 +154,39 @@ class Neo4jTemplateIT { Node node = Cypher.node("PersonWithAllConstructor").named("n"); Statement statement = Cypher.match(node).returning(Cypher.count(node)).build(); - assertThat(neo4jTemplate.count(statement)).isEqualTo(2); + assertThat(this.neo4jTemplate.count(statement)).isEqualTo(2); } @Test void countWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(Cypher.parameter("name"))) - .returning(Cypher.count(node)).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(Cypher.parameter("name"))) + .returning(Cypher.count(node)) + .build(); - assertThat(neo4jTemplate.count(statement, Collections.singletonMap("name", TEST_PERSON1_NAME))).isEqualTo(1); + assertThat(this.neo4jTemplate.count(statement, Collections.singletonMap("name", TEST_PERSON1_NAME))) + .isEqualTo(1); } @Test void countWithCypherQuery() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) return count(p)"; - assertThat(neo4jTemplate.count(cypherQuery)).isEqualTo(2); + assertThat(this.neo4jTemplate.count(cypherQuery)).isEqualTo(2); } @Test void countWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return count(p)"; - assertThat(neo4jTemplate.count(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME))).isEqualTo(1); + assertThat(this.neo4jTemplate.count(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME))) + .isEqualTo(1); } @Test void findAll() { - List people = neo4jTemplate.findAll(PersonWithAllConstructor.class); + List people = this.neo4jTemplate.findAll(PersonWithAllConstructor.class); assertThat(people).hasSize(2); } @@ -168,19 +195,20 @@ class Neo4jTemplateIT { Node node = Cypher.node("PersonWithAllConstructor").named("n"); Statement statement = Cypher.match(node).returning(node).build(); - List people = neo4jTemplate.findAll(statement, PersonWithAllConstructor.class); + List people = this.neo4jTemplate.findAll(statement, PersonWithAllConstructor.class); assertThat(people).hasSize(2); } @Test void findAllWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(Cypher.parameter("name"))) - .returning(node).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(Cypher.parameter("name"))) + .returning(node) + .build(); - List people = neo4jTemplate.findAll(statement, Collections - .singletonMap("name", TEST_PERSON1_NAME), - PersonWithAllConstructor.class); + List people = this.neo4jTemplate.findAll(statement, + Collections.singletonMap("name", TEST_PERSON1_NAME), PersonWithAllConstructor.class); assertThat(people).hasSize(1); } @@ -188,10 +216,12 @@ class Neo4jTemplateIT { @Test void findOneWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(Cypher.parameter("name"))) - .returning(node).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(Cypher.parameter("name"))) + .returning(node) + .build(); - Optional person = neo4jTemplate.findOne(statement, + Optional person = this.neo4jTemplate.findOne(statement, Collections.singletonMap("name", TEST_PERSON1_NAME), PersonWithAllConstructor.class); assertThat(person).isPresent(); @@ -200,10 +230,12 @@ class Neo4jTemplateIT { @Test // 2230 void findAllWithStatementWithoutParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(Cypher.parameter("name").withValue(TEST_PERSON1_NAME))) - .returning(node).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(Cypher.parameter("name").withValue(TEST_PERSON1_NAME))) + .returning(node) + .build(); - List people = neo4jTemplate.findAll(statement, PersonWithAllConstructor.class); + List people = this.neo4jTemplate.findAll(statement, PersonWithAllConstructor.class); assertThat(people).hasSize(1); } @@ -212,7 +244,7 @@ class Neo4jTemplateIT { void findAllWithCypherQuery() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) return p"; - List people = neo4jTemplate.findAll(cypherQuery, PersonWithAllConstructor.class); + List people = this.neo4jTemplate.findAll(cypherQuery, PersonWithAllConstructor.class); assertThat(people).hasSize(2); } @@ -220,7 +252,7 @@ class Neo4jTemplateIT { void findAllWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return p"; - List people = neo4jTemplate.findAll(cypherQuery, + List people = this.neo4jTemplate.findAll(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), PersonWithAllConstructor.class); assertThat(people).hasSize(1); @@ -230,7 +262,7 @@ class Neo4jTemplateIT { void findOneWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return p"; - Optional person = neo4jTemplate.findOne(cypherQuery, + Optional person = this.neo4jTemplate.findOne(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), PersonWithAllConstructor.class); assertThat(person).isPresent(); @@ -238,26 +270,27 @@ class Neo4jTemplateIT { @Test void findById() { - Optional person = neo4jTemplate.findById(person1Id, PersonWithAllConstructor.class); + Optional person = this.neo4jTemplate.findById(this.person1Id, + PersonWithAllConstructor.class); assertThat(person).isPresent(); } @Test void findAllById() { - List people = neo4jTemplate.findAllById(Arrays.asList(person1Id, person2Id), - PersonWithAllConstructor.class); + List people = this.neo4jTemplate + .findAllById(Arrays.asList(this.person1Id, this.person2Id), PersonWithAllConstructor.class); assertThat(people).hasSize(2); } @Test void save() { - ThingWithGeneratedId testThing = neo4jTemplate.save(new ThingWithGeneratedId("testThing")); + ThingWithGeneratedId testThing = this.neo4jTemplate.save(new ThingWithGeneratedId("testThing")); assertThat(testThing.getTheId()).isNotNull(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (t:ThingWithGeneratedId{name: 'testThing'}) return t"); Value resultValue = result.single().get("t"); assertThat(resultValue).isNotNull(); @@ -271,31 +304,32 @@ class Neo4jTemplateIT { String thing2Name = "testThing2"; ThingWithGeneratedId thing1 = new ThingWithGeneratedId(thing1Name); ThingWithGeneratedId thing2 = new ThingWithGeneratedId(thing2Name); - List savedThings = neo4jTemplate.saveAll(Arrays.asList(thing1, thing2)); + List savedThings = this.neo4jTemplate.saveAll(Arrays.asList(thing1, thing2)); assertThat(savedThings).hasSize(2); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Map paramMap = new HashMap<>(); paramMap.put("name1", thing1Name); paramMap.put("name2", thing2Name); Result result = session - .run("MATCH (t:ThingWithGeneratedId) WHERE t.name = $name1 or t.name = $name2 return t", - paramMap); + .run("MATCH (t:ThingWithGeneratedId) WHERE t.name = $name1 or t.name = $name2 return t", paramMap); List resultValues = result.list(); assertThat(resultValues).hasSize(2); - assertThat(resultValues).allMatch( - record -> record.asMap(Function.identity()).get("t").get("name").asString() - .startsWith("testThing")); + assertThat(resultValues).allMatch(record -> record.asMap(Function.identity()) + .get("t") + .get("name") + .asString() + .startsWith("testThing")); } } @Test void deleteById() { - neo4jTemplate.deleteById(person1Id, PersonWithAllConstructor.class); + this.neo4jTemplate.deleteById(this.person1Id, PersonWithAllConstructor.class); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithAllConstructor) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(1); } @@ -303,25 +337,603 @@ class Neo4jTemplateIT { @Test void deleteAllById() { - neo4jTemplate.deleteAllById(Arrays.asList(person1Id, person2Id), PersonWithAllConstructor.class); + this.neo4jTemplate.deleteAllById(Arrays.asList(this.person1Id, this.person2Id), PersonWithAllConstructor.class); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithAllConstructor) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); } } + @Test // GH-2215 + void saveProjectionShouldWork() { + + // Using a query on purpose so that the address is null + DtoPersonProjection dtoPersonProjection = this.neo4jTemplate.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Siemons")) + .one() + .get(); + + dtoPersonProjection.setFirstName("Micha"); + dtoPersonProjection.setLastName("Simons"); + + DtoPersonProjection savedProjection = this.neo4jTemplate.save(Person.class).one(dtoPersonProjection); + + // Assert that we saved and returned the correct data + assertThat(savedProjection.getFirstName()).isEqualTo("Micha"); + assertThat(savedProjection.getLastName()).isEqualTo("Simons"); + + // Assert the result inside the database. + Person person = this.neo4jTemplate.findById(savedProjection.getId(), Person.class).get(); + assertThat(person.getFirstName()).isEqualTo("Micha"); + assertThat(person.getLastName()).isEqualTo("Simons"); + assertThat(person.getAddress()).isNotNull(); + } + + @Test // GH-2505 + void savePrimitivesShouldWork() { + EntityWithPrimitiveConstructorArguments entity = new EntityWithPrimitiveConstructorArguments(true, 42); + EntityWithPrimitiveConstructorArguments savedEntity = this.neo4jTemplate + .save(EntityWithPrimitiveConstructorArguments.class) + .one(entity); + + assertThat(savedEntity.someIntValue).isEqualTo(entity.someIntValue); + assertThat(savedEntity.someBooleanValue).isEqualTo(entity.someBooleanValue); + } + + @Test // GH-2215 + void saveAllProjectionShouldWork() { + + // Using a query on purpose so that the address is null + DtoPersonProjection dtoPersonProjection = this.neo4jTemplate.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Siemons")) + .one() + .get(); + + dtoPersonProjection.setFirstName("Micha"); + dtoPersonProjection.setLastName("Simons"); + + Iterable savedProjections = this.neo4jTemplate.save(Person.class) + .all(Collections.singleton(dtoPersonProjection)); + + DtoPersonProjection savedProjection = savedProjections.iterator().next(); + // Assert that we saved and returned the correct data + assertThat(savedProjection.getFirstName()).isEqualTo("Micha"); + assertThat(savedProjection.getLastName()).isEqualTo("Simons"); + + // Assert the result inside the database. + Person person = this.neo4jTemplate.findById(savedProjection.getId(), Person.class).get(); + assertThat(person.getFirstName()).isEqualTo("Micha"); + assertThat(person.getLastName()).isEqualTo("Simons"); + assertThat(person.getAddress()).isNotNull(); + } + + @Test + void saveAsWithOpenProjectionShouldWork() { + + // Using a query on purpose so that the address is null + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + + p.setFirstName("Micha"); + p.setLastName("Simons"); + OpenProjection openProjection = this.neo4jTemplate.saveAs(p, OpenProjection.class); + + assertThat(openProjection.getFullName()).isEqualTo("Michael Simons"); + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNotNull(); + } + + @Test + void saveAllAsWithOpenProjectionShouldWork() { + + // Using a query on purpose so that the address is null + Person p1 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + Person p2 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class) + .get(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + + List openProjections = this.neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), + OpenProjection.class); + + assertThat(openProjections).extracting(OpenProjection::getFullName) + .containsExactlyInAnyOrder("Michael Simons", "Helge Schneider"); + + List people = this.neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); + + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() != null); + } + + @Test + void saveAsWithSameClassShouldWork() { + + // Using a query on purpose so that the address is null + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + + p.setFirstName("Micha"); + p.setLastName("Simons"); + Person savedInstance = this.neo4jTemplate.saveAs(p, Person.class); + + assertThat(savedInstance.getFirstName()).isEqualTo("Micha"); + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getFirstName()).isEqualTo("Micha"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNull(); + } + + @Test + void saveAllAsWithSameClassShouldWork() { + + // Using a query on purpose so that the address is null + Person p1 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + Person p2 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class) + .get(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + + List openProjection = this.neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), Person.class); + + assertThat(openProjection).extracting(Person::getFirstName).containsExactlyInAnyOrder("Micha", "Helga"); + + List people = this.neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); + + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Micha", "Helga"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() == null); + } + + @Test + void saveAsWithClosedProjectionShouldWork() { + + // Using a query on purpose so that the address is null + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + + p.setFirstName("Micha"); + p.setLastName("Simons"); + ClosedProjection closedProjection = this.neo4jTemplate.saveAs(p, ClosedProjection.class); + + assertThat(closedProjection.getLastName()).isEqualTo("Simons"); + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNotNull(); + } + + @Test + void saveAsWithClosedProjectionOnSecondLevelShouldWork() { + + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + ClosedProjectionWithEmbeddedProjection projection = this.neo4jTemplate.saveAs(p, + ClosedProjectionWithEmbeddedProjection.class); + + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + } + + @Test // GH-2420 + void saveAsWithDynamicProjectionOnSecondLevelShouldWork() { + + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + Person.Address.Country country = new Person.Address.Country(); + country.setName("Foo"); + country.setCountryCode("DE"); + p.getAddress().setCountry(country); + + BiPredicate predicate = create2LevelProjectingPredicate(); + + Person projection = this.neo4jTemplate.saveAs(p, predicate); + + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Foo"); + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); + } + + @Test // GH-2407 + void saveAllAsWithClosedProjectionOnSecondLevelShouldWork() { + + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + p.setFirstName("Klaus"); + p.setLastName("Simons"); + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + List projections = this.neo4jTemplate + .saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class); + + assertThat(projections).hasSize(1) + .first() + .satisfies(projection -> assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail")); + + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + } + + @Test // GH-2420 + void saveAllAsWithDynamicProjectionOnSecondLevelShouldWork() { + + Person p = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + p.setFirstName("Klaus"); + p.setLastName("Simons"); + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + Person.Address.Country country = new Person.Address.Country(); + country.setName("Foo"); + country.setCountryCode("DE"); + p.getAddress().setCountry(country); + + BiPredicate predicate = create2LevelProjectingPredicate(); + + List projections = this.neo4jTemplate.saveAllAs(Collections.singletonList(p), predicate); + + assertThat(projections).hasSize(1).first().satisfies(projection -> { + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Foo"); + }); + + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); + } + + @Test // GH-2407 + void shouldSaveNewProjectedThing() { + + Person p = new Person(); + p.setFirstName("John"); + p.setLastName("Doe"); + + ClosedProjection projection = this.neo4jTemplate.saveAs(p, ClosedProjection.class); + List people = this.neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Doe"), Person.class); + assertThat(people).hasSize(1).first().satisfies(person -> { + assertThat(person.getFirstName()).isNull(); + assertThat(person.getLastName()).isEqualTo(projection.getLastName()); + }); + } + + @Test // GH-2407 + void shouldSaveAllNewProjectedThings() { + + Person p = new Person(); + p.setFirstName("John"); + p.setLastName("Doe"); + + List projections = this.neo4jTemplate.saveAllAs(Collections.singletonList(p), + ClosedProjection.class); + assertThat(projections).hasSize(1); + + ClosedProjection projection = projections.get(0); + List people = this.neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Doe"), Person.class); + assertThat(people).hasSize(1).first().satisfies(person -> { + assertThat(person.getFirstName()).isNull(); + assertThat(person.getLastName()).isEqualTo(projection.getLastName()); + }); + } + + @Test // GH-2407 + void shouldSaveAllAsWithAssignedIdProjected() { + + PersonWithAssignedId p = this.neo4jTemplate.findById("x", PersonWithAssignedId.class).get(); + p.setLastName("modifiedLast"); + p.setFirstName("modifiedFirst"); + + List projections = this.neo4jTemplate.saveAllAs(Collections.singletonList(p), + ClosedProjection.class); + assertThat(projections).hasSize(1); + + ClosedProjection projection = projections.get(0); + List people = this.neo4jTemplate.findAll( + "MATCH (p:PersonWithAssignedId {id: $id}) RETURN p", Collections.singletonMap("id", "x"), + PersonWithAssignedId.class); + assertThat(people).hasSize(1).first().satisfies(person -> { + assertThat(person.getFirstName()).isEqualTo("John"); + assertThat(person.getLastName()).isEqualTo(projection.getLastName()); + }); + } + + @Test // GH-2407 + void shouldSaveAsWithAssignedIdProjected() { + + PersonWithAssignedId p = this.neo4jTemplate.findById("x", PersonWithAssignedId.class).get(); + p.setLastName("modifiedLast"); + p.setFirstName("modifiedFirst"); + + ClosedProjection projection = this.neo4jTemplate.saveAs(p, ClosedProjection.class); + List people = this.neo4jTemplate.findAll( + "MATCH (p:PersonWithAssignedId {id: $id}) RETURN p", Collections.singletonMap("id", "x"), + PersonWithAssignedId.class); + assertThat(people).hasSize(1).first().satisfies(person -> { + assertThat(person.getFirstName()).isEqualTo("John"); + assertThat(person.getLastName()).isEqualTo(projection.getLastName()); + }); + } + + @Test + void saveAsWithClosedProjectionOnThreeLevelShouldWork() { + + Person p = this.neo4jTemplate.findOne( + "MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + Person.Address.Country country = p.getAddress().getCountry(); + country.setName("Germany"); + country.setCountryCode("AT"); + + ClosedProjectionWithEmbeddedProjection projection = this.neo4jTemplate.saveAs(p, + ClosedProjectionWithEmbeddedProjection.class); + assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Germany"); + + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + Person.Address.Country savedCountry = p.getAddress().getCountry(); + assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); + assertThat(savedCountry.getName()).isEqualTo("Germany"); + } + + @Test // GH-2407 + void saveAllAsWithClosedProjectionOnThreeLevelShouldWork() { + + Person p = this.neo4jTemplate.findOne( + "MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .get(); + + Person.Address.Country country = p.getAddress().getCountry(); + country.setName("Germany"); + country.setCountryCode("AT"); + + List projections = this.neo4jTemplate + .saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class); + + assertThat(projections).hasSize(1) + .first() + .satisfies(projection -> assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Germany")); + + p = this.neo4jTemplate.findById(p.getId(), Person.class).get(); + Person.Address.Country savedCountry = p.getAddress().getCountry(); + assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); + assertThat(savedCountry.getName()).isEqualTo("Germany"); + } + + @Test + void saveAllAsWithClosedProjectionShouldWork() { + + // Using a query on purpose so that the address is null + Person p1 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .get(); + Person p2 = this.neo4jTemplate + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class) + .get(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + + List closedProjections = this.neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), + ClosedProjection.class); + + assertThat(closedProjections).extracting(ClosedProjection::getLastName) + .containsExactlyInAnyOrder("Simons", "Schneider"); + + List people = this.neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); + + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() != null); + } + + @Test // GH-2544 + void saveAllAsWithEmptyList() { + List projections = this.neo4jTemplate.saveAllAs(Collections.emptyList(), + ClosedProjection.class); + + assertThat(projections).isEmpty(); + } + + @Test // GH-2544 + void saveWeirdHierarchy() { + + List things = new ArrayList<>(); + things.add(new X()); + things.add(new Y()); + + assertThatIllegalArgumentException() + .isThrownBy(() -> this.neo4jTemplate.saveAllAs(things, ClosedProjection.class)) + .withMessage("Could not determine a common element of an heterogeneous collection"); + } + + @Test + void updatingFindShouldWork(@Autowired PlatformTransactionManager transactionManager) { + Map params = new HashMap<>(); + params.put("wrongName", "Siemons"); + params.put("correctName", "Simons"); + new TransactionTemplate(transactionManager).executeWithoutResult(tx -> { + Optional optionalResult = this.neo4jTemplate.findOne( + "MERGE (p:Person {lastName: $wrongName}) ON MATCH set p.lastName = $correctName RETURN p", params, + Person.class); + + assertThat(optionalResult).hasValueSatisfying(updatedPerson -> { + assertThat(updatedPerson.getLastName()).isEqualTo("Simons"); + assertThat(updatedPerson.getAddress()).isNull(); // We didn't fetch it + }); + }); + } + + @Test + void executableFindShouldWorkAllDomainObjectsShouldWork() { + List people = this.neo4jTemplate.find(Person.class).all(); + assertThat(people).hasSize(4); + } + + @Test + void executableFindShouldWorkAllDomainObjectsProjectedShouldWork() { + List people = this.neo4jTemplate.find(Person.class).as(OpenProjection.class).all(); + assertThat(people).extracting(OpenProjection::getFullName) + .containsExactlyInAnyOrder("Helge Schnitzel", "Michael Siemons", "Bela B.", "A LA"); + } + + @Test // GH-2270 + void executableFindShouldWorkAllDomainObjectsProjectedDTOShouldWork() { + List people = this.neo4jTemplate.find(Person.class).as(DtoPersonProjection.class).all(); + assertThat(people).extracting(DtoPersonProjection::getLastName) + .containsExactlyInAnyOrder("Schnitzel", "Siemons", "B.", "LA"); + } + + @Test // GH-2270 + void executableFindShouldWorkOneDomainObjectsProjectedDTOShouldWork() { + Optional person = this.neo4jTemplate.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel")) + .one(); + assertThat(person).map(DtoPersonProjection::getLastName).hasValue("Schnitzel"); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQuery() { + List people = this.neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1").all(); + assertThat(people).hasSize(1); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQueryAndParam() { + List people = this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel")) + .all(); + assertThat(people).hasSize(1); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQueryAndNullParams() { + List people = this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p LIMIT 1", null) + .all(); + assertThat(people).hasSize(1); + } + + @Test + void oneShouldWork() { + Optional people = this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p LIMIT 1") + .one(); + assertThat(people).isPresent(); + } + + @Test + void oneShouldWorkWithIncorrectResultSize() { + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> this.neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p").one()); + } + + @Test + void statementShouldWork() { + Node person = Cypher.node("Person"); + List people = this.neo4jTemplate.find(Person.class) + .matching(Cypher.match(person) + .where(person.property("lastName").isEqualTo(Cypher.anonParameter("Siemons"))) + .returning(person) + .build()) + .all(); + assertThat(people).extracting(Person::getLastName).containsExactly("Siemons"); + } + + @Test + void statementWithParamsShouldWork() { + Node person = Cypher.node("Person"); + List people = this.neo4jTemplate.find(Person.class) + .matching(Cypher.match(person) + .where(person.property("lastName").isEqualTo(Cypher.parameter("lastName", "Siemons"))) + .returning(person) + .build(), Collections.singletonMap("lastName", "Schnitzel")) + .all(); + assertThat(people).extracting(Person::getLastName).containsExactly("Schnitzel"); + } + interface OpenProjection { String getLastName(); @org.springframework.beans.factory.annotation.Value("#{target.firstName + ' ' + target.lastName}") String getFullName(); + } interface ClosedProjection { String getLastName(); + } interface ClosedProjectionWithEmbeddedProjection { @@ -337,22 +949,16 @@ class Neo4jTemplateIT { CountryProjection getCountry(); interface CountryProjection { + String getName(); + } + } + } - private static BiPredicate create2LevelProjectingPredicate() { - BiPredicate predicate = (path, property) -> false; - predicate = predicate.or((path, property) -> property.getName().equals("lastName")); - predicate = predicate.or((path, property) -> property.getName().equals("address") - || path.toDotPath().startsWith("address.") && property.getName().equals("street")); - predicate = predicate.or((path, property) -> property.getName().equals("country") - || path.toDotPath().contains("address.country.") && property.getName().equals("name")); - return predicate; - } - - static class DtoPersonProjection { + public static final class DtoPersonProjection { /** * The ID is required in a project that should be saved. @@ -360,6 +966,7 @@ class Neo4jTemplateIT { private final Long id; private String lastName; + private String firstName; DtoPersonProjection(Long id) { @@ -374,18 +981,23 @@ class Neo4jTemplateIT { return this.lastName; } - public String getFirstName() { - return this.firstName; - } - public void setLastName(String lastName) { this.lastName = lastName; } + public String getFirstName() { + return this.firstName; + } + public void setFirstName(String firstName) { this.firstName = firstName; } + protected boolean canEqual(final Object other) { + return other instanceof DtoPersonProjection; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -399,587 +1011,46 @@ class Neo4jTemplateIT { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$lastName = this.getLastName(); final Object other$lastName = other.getLastName(); - if (this$lastName == null ? other$lastName != null : !this$lastName.equals(other$lastName)) { + if (!Objects.equals(this$lastName, other$lastName)) { return false; } final Object this$firstName = this.getFirstName(); final Object other$firstName = other.getFirstName(); - if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof DtoPersonProjection; + return Objects.equals(this$firstName, other$firstName); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $lastName = this.getLastName(); - result = result * PRIME + ($lastName == null ? 43 : $lastName.hashCode()); + result = result * PRIME + (($lastName != null) ? $lastName.hashCode() : 43); final Object $firstName = this.getFirstName(); - result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode()); + result = result * PRIME + (($firstName != null) ? $firstName.hashCode() : 43); return result; } + @Override public String toString() { - return "Neo4jTemplateIT.DtoPersonProjection(id=" + this.getId() + ", lastName=" + this.getLastName() + ", firstName=" + this.getFirstName() + ")"; + return "Neo4jTemplateIT.DtoPersonProjection(id=" + this.getId() + ", lastName=" + this.getLastName() + + ", firstName=" + this.getFirstName() + ")"; } - } - @Test // GH-2215 - void saveProjectionShouldWork() { - - // Using a query on purpose so that the address is null - DtoPersonProjection dtoPersonProjection = neo4jTemplate - .find(Person.class) - .as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons")) - .one() - .get(); - - dtoPersonProjection.setFirstName("Micha"); - dtoPersonProjection.setLastName("Simons"); - - DtoPersonProjection savedProjection = neo4jTemplate - .save(Person.class) - .one(dtoPersonProjection); - - // Assert that we saved and returned the correct data - assertThat(savedProjection.getFirstName()).isEqualTo("Micha"); - assertThat(savedProjection.getLastName()).isEqualTo("Simons"); - - // Assert the result inside the database. - Person person = neo4jTemplate.findById(savedProjection.getId(), Person.class).get(); - assertThat(person.getFirstName()).isEqualTo("Micha"); - assertThat(person.getLastName()).isEqualTo("Simons"); - assertThat(person.getAddress()).isNotNull(); - } - - @Test // GH-2505 - void savePrimitivesShouldWork() { - EntityWithPrimitiveConstructorArguments entity = new EntityWithPrimitiveConstructorArguments(true, 42); - EntityWithPrimitiveConstructorArguments savedEntity = neo4jTemplate.save(EntityWithPrimitiveConstructorArguments.class).one(entity); - - assertThat(savedEntity.someIntValue).isEqualTo(entity.someIntValue); - assertThat(savedEntity.someBooleanValue).isEqualTo(entity.someBooleanValue); - } - - @Test // GH-2215 - void saveAllProjectionShouldWork() { - - // Using a query on purpose so that the address is null - DtoPersonProjection dtoPersonProjection = neo4jTemplate - .find(Person.class) - .as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons")) - .one() - .get(); - - dtoPersonProjection.setFirstName("Micha"); - dtoPersonProjection.setLastName("Simons"); - - Iterable savedProjections = neo4jTemplate - .save(Person.class) - .all(Collections.singleton(dtoPersonProjection)); - - DtoPersonProjection savedProjection = savedProjections.iterator().next(); - // Assert that we saved and returned the correct data - assertThat(savedProjection.getFirstName()).isEqualTo("Micha"); - assertThat(savedProjection.getLastName()).isEqualTo("Simons"); - - // Assert the result inside the database. - Person person = neo4jTemplate.findById(savedProjection.getId(), Person.class).get(); - assertThat(person.getFirstName()).isEqualTo("Micha"); - assertThat(person.getLastName()).isEqualTo("Simons"); - assertThat(person.getAddress()).isNotNull(); - } - - @Test - void saveAsWithOpenProjectionShouldWork() { - - // Using a query on purpose so that the address is null - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.setFirstName("Micha"); - p.setLastName("Simons"); - OpenProjection openProjection = neo4jTemplate.saveAs(p, OpenProjection.class); - - assertThat(openProjection.getFullName()).isEqualTo("Michael Simons"); - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNotNull(); - } - - @Test - void saveAllAsWithOpenProjectionShouldWork() { - - // Using a query on purpose so that the address is null - Person p1 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - Person p2 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class).get(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - - List openProjections = neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), OpenProjection.class); - - assertThat(openProjections).extracting(OpenProjection::getFullName) - .containsExactlyInAnyOrder("Michael Simons", "Helge Schneider"); - - List people = neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); - - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() != null); - } - - @Test - void saveAsWithSameClassShouldWork() { - - // Using a query on purpose so that the address is null - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.setFirstName("Micha"); - p.setLastName("Simons"); - Person savedInstance = neo4jTemplate.saveAs(p, Person.class); - - assertThat(savedInstance.getFirstName()).isEqualTo("Micha"); - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getFirstName()).isEqualTo("Micha"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNull(); - } - - @Test - void saveAllAsWithSameClassShouldWork() { - - // Using a query on purpose so that the address is null - Person p1 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - Person p2 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class).get(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - - List openProjection = neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), Person.class); - - assertThat(openProjection).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Micha", "Helga"); - - List people = neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); - - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Micha", "Helga"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() == null); - } - - @Test - void saveAsWithClosedProjectionShouldWork() { - - // Using a query on purpose so that the address is null - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.setFirstName("Micha"); - p.setLastName("Simons"); - ClosedProjection closedProjection = neo4jTemplate.saveAs(p, ClosedProjection.class); - - assertThat(closedProjection.getLastName()).isEqualTo("Simons"); - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNotNull(); - } - - @Test - void saveAsWithClosedProjectionOnSecondLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - ClosedProjectionWithEmbeddedProjection projection = neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); - - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - } - - @Test // GH-2420 - void saveAsWithDynamicProjectionOnSecondLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - Person.Address.Country country = new Person.Address.Country(); - country.setName("Foo"); - country.setCountryCode("DE"); - p.getAddress().setCountry(country); - - BiPredicate predicate = create2LevelProjectingPredicate(); - - Person projection = neo4jTemplate.saveAs(p, predicate); - - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Foo"); - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); - } - - @Test // GH-2407 - void saveAllAsWithClosedProjectionOnSecondLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.setFirstName("Klaus"); - p.setLastName("Simons"); - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - List projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class); - - assertThat(projections) - .hasSize(1).first() - .satisfies(projection -> assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail")); - - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - } - - @Test // GH-2420 - void saveAllAsWithDynamicProjectionOnSecondLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - p.setFirstName("Klaus"); - p.setLastName("Simons"); - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - Person.Address.Country country = new Person.Address.Country(); - country.setName("Foo"); - country.setCountryCode("DE"); - p.getAddress().setCountry(country); - - BiPredicate predicate = create2LevelProjectingPredicate(); - - List projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), predicate); - - assertThat(projections) - .hasSize(1).first() - .satisfies(projection -> { - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Foo"); - }); - - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); - } - - @Test // GH-2407 - void shouldSaveNewProjectedThing() { - - Person p = new Person(); - p.setFirstName("John"); - p.setLastName("Doe"); - - ClosedProjection projection = neo4jTemplate.saveAs(p, ClosedProjection.class); - List people = neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Doe"), Person.class); - assertThat(people).hasSize(1) - .first().satisfies(person -> { - assertThat(person.getFirstName()).isNull(); - assertThat(person.getLastName()).isEqualTo(projection.getLastName()); - }); - } - - @Test // GH-2407 - void shouldSaveAllNewProjectedThings() { - - Person p = new Person(); - p.setFirstName("John"); - p.setLastName("Doe"); - - List projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), - ClosedProjection.class); - assertThat(projections).hasSize(1); - - ClosedProjection projection = projections.get(0); - List people = neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Doe"), Person.class); - assertThat(people).hasSize(1) - .first().satisfies(person -> { - assertThat(person.getFirstName()).isNull(); - assertThat(person.getLastName()).isEqualTo(projection.getLastName()); - }); - } - - @Test // GH-2407 - void shouldSaveAllAsWithAssignedIdProjected() { - - PersonWithAssignedId p = neo4jTemplate.findById("x", PersonWithAssignedId.class).get(); - p.setLastName("modifiedLast"); - p.setFirstName("modifiedFirst"); - - List projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), - ClosedProjection.class); - assertThat(projections).hasSize(1); - - ClosedProjection projection = projections.get(0); - List people = neo4jTemplate.findAll("MATCH (p:PersonWithAssignedId {id: $id}) RETURN p", - Collections.singletonMap("id", "x"), PersonWithAssignedId.class); - assertThat(people).hasSize(1) - .first().satisfies(person -> { - assertThat(person.getFirstName()).isEqualTo("John"); - assertThat(person.getLastName()).isEqualTo(projection.getLastName()); - }); - } - - @Test // GH-2407 - void shouldSaveAsWithAssignedIdProjected() { - - PersonWithAssignedId p = neo4jTemplate.findById("x", PersonWithAssignedId.class).get(); - p.setLastName("modifiedLast"); - p.setFirstName("modifiedFirst"); - - ClosedProjection projection = neo4jTemplate.saveAs(p, ClosedProjection.class); - List people = neo4jTemplate.findAll("MATCH (p:PersonWithAssignedId {id: $id}) RETURN p", - Collections.singletonMap("id", "x"), PersonWithAssignedId.class); - assertThat(people).hasSize(1) - .first().satisfies(person -> { - assertThat(person.getFirstName()).isEqualTo("John"); - assertThat(person.getLastName()).isEqualTo(projection.getLastName()); - }); - } - - @Test - void saveAsWithClosedProjectionOnThreeLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - Person.Address.Country country = p.getAddress().getCountry(); - country.setName("Germany"); - country.setCountryCode("AT"); - - ClosedProjectionWithEmbeddedProjection projection = neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); - assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Germany"); - - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - Person.Address.Country savedCountry = p.getAddress().getCountry(); - assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); - assertThat(savedCountry.getName()).isEqualTo("Germany"); - } - - @Test // GH-2407 - void saveAllAsWithClosedProjectionOnThreeLevelShouldWork() { - - Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - - Person.Address.Country country = p.getAddress().getCountry(); - country.setName("Germany"); - country.setCountryCode("AT"); - - List projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class); - - assertThat(projections) - .hasSize(1).first() - .satisfies(projection -> assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Germany")); - - p = neo4jTemplate.findById(p.getId(), Person.class).get(); - Person.Address.Country savedCountry = p.getAddress().getCountry(); - assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); - assertThat(savedCountry.getName()).isEqualTo("Germany"); - } - - @Test - void saveAllAsWithClosedProjectionShouldWork() { - - // Using a query on purpose so that the address is null - Person p1 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class).get(); - Person p2 = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class).get(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - - List closedProjections = neo4jTemplate - .saveAllAs(Arrays.asList(p1, p2), ClosedProjection.class); - - assertThat(closedProjections).extracting(ClosedProjection::getLastName) - .containsExactlyInAnyOrder("Simons", "Schneider"); - - List people = neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class); - - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() != null); - } - - @Test // GH-2544 - void saveAllAsWithEmptyList() { - List projections = neo4jTemplate.saveAllAs(Collections.emptyList(), ClosedProjection.class); - - assertThat(projections).isEmpty(); } static class X { + } static class Y { - } - @Test // GH-2544 - void saveWeirdHierarchy() { - - List things = new ArrayList<>(); - things.add(new X()); - things.add(new Y()); - - assertThatIllegalArgumentException().isThrownBy(() -> neo4jTemplate.saveAllAs(things, ClosedProjection.class)) - .withMessage("Could not determine a common element of an heterogeneous collection"); - } - - @Test - void updatingFindShouldWork(@Autowired PlatformTransactionManager transactionManager) { - Map params = new HashMap<>(); - params.put("wrongName", "Siemons"); - params.put("correctName", "Simons"); - new TransactionTemplate(transactionManager).executeWithoutResult(tx -> { - Optional optionalResult = neo4jTemplate - .findOne("MERGE (p:Person {lastName: $wrongName}) ON MATCH set p.lastName = $correctName RETURN p", - params, Person.class); - - assertThat(optionalResult).hasValueSatisfying( - updatedPerson -> { - assertThat(updatedPerson.getLastName()).isEqualTo("Simons"); - assertThat(updatedPerson.getAddress()).isNull(); // We didn't fetch it - } - ); - }); - } - - @Test - void executableFindShouldWorkAllDomainObjectsShouldWork() { - List people = neo4jTemplate.find(Person.class).all(); - assertThat(people).hasSize(4); - } - - @Test - void executableFindShouldWorkAllDomainObjectsProjectedShouldWork() { - List people = neo4jTemplate.find(Person.class).as(OpenProjection.class).all(); - assertThat(people).extracting(OpenProjection::getFullName) - .containsExactlyInAnyOrder("Helge Schnitzel", "Michael Siemons", "Bela B.", "A LA"); - } - - @Test // GH-2270 - void executableFindShouldWorkAllDomainObjectsProjectedDTOShouldWork() { - List people = neo4jTemplate.find(Person.class).as(DtoPersonProjection.class).all(); - assertThat(people).extracting(DtoPersonProjection::getLastName) - .containsExactlyInAnyOrder("Schnitzel", "Siemons", "B.", "LA"); - } - - @Test // GH-2270 - void executableFindShouldWorkOneDomainObjectsProjectedDTOShouldWork() { - Optional person = neo4jTemplate - .find(Person.class).as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Schnitzel")) - .one(); - assertThat(person).map(DtoPersonProjection::getLastName) - .hasValue("Schnitzel"); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQuery() { - List people = neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1").all(); - assertThat(people).hasSize(1); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQueryAndParam() { - List people = neo4jTemplate.find(Person.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel")).all(); - assertThat(people).hasSize(1); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQueryAndNullParams() { - List people = neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1", null) - .all(); - assertThat(people).hasSize(1); - } - - @Test - void oneShouldWork() { - Optional people = neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1") - .one(); - assertThat(people).isPresent(); - } - - @Test - void oneShouldWorkWithIncorrectResultSize() { - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) - .isThrownBy(() -> neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p").one()); - } - - @Test - void statementShouldWork() { - Node person = Cypher.node("Person"); - List people = neo4jTemplate.find(Person.class).matching(Cypher.match(person) - .where(person.property("lastName").isEqualTo(Cypher.anonParameter("Siemons"))) - .returning(person).build()) - .all(); - assertThat(people).extracting(Person::getLastName).containsExactly("Siemons"); - } - - @Test - void statementWithParamsShouldWork() { - Node person = Cypher.node("Person"); - List people = neo4jTemplate.find(Person.class).matching(Cypher.match(person) - .where(person.property("lastName").isEqualTo(Cypher.parameter("lastName", "Siemons"))) - .returning(person).build(), Collections.singletonMap("lastName", "Schnitzel")) - .all(); - assertThat(people).extracting(Person::getLastName).containsExactly("Schnitzel"); } @Configuration @@ -992,21 +1063,24 @@ class Neo4jTemplateIT { return neo4jConnectionSupport.getDriver(); } - @Override // needed here because there is no implicit registration of entities upfront some methods under test + @Override // needed here because there is no implicit registration of entities + // upfront some methods under test protected Collection getMappingBasePackages() { return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); transactionManager.setValidateExistingTransaction(true); return transactionManager; } @@ -1015,5 +1089,7 @@ class Neo4jTemplateIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTransactionManagerTestIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTransactionManagerTestIT.java index fe37b8dc9..5104174bb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTransactionManagerTestIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTransactionManagerTestIT.java @@ -15,18 +15,15 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -37,12 +34,16 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Michael J. Simons */ @@ -60,32 +61,31 @@ class Neo4jTransactionManagerTestIT { } @Test // GH-2193 - void exceptionShouldNotBeShadowed( - @Autowired TransactionTemplate transactionTemplate, - @Autowired Neo4jClient client, - @Autowired BookmarkCapture bookmarkCapture, - @Autowired SomeRepository someRepository) { + void exceptionShouldNotBeShadowed(@Autowired TransactionTemplate transactionTemplate, @Autowired Neo4jClient client, + @Autowired BookmarkCapture bookmarkCapture, @Autowired SomeRepository someRepository) { assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> - // Need to wrap so that we actually trigger the setRollBackOnly on the outer transaction - transactionTemplate.executeWithoutResult(tx -> { - client.query("CREATE (n:ShouldNotBeThere)").run(); - someRepository.broken(); - })).withMessageStartingWith("Invalid input"); + // Need to wrap so that we actually trigger the setRollBackOnly on the outer + // transaction + transactionTemplate.executeWithoutResult(tx -> { + client.query("CREATE (n:ShouldNotBeThere)").run(); + someRepository.broken(); + })).withMessageStartingWith("Invalid input"); try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - long cnt = session - .executeRead(tx -> tx.run("MATCH (n:ShouldNotBeThere) RETURN count(n)").single().get(0)) - .asLong(); + long cnt = session.executeRead(tx -> tx.run("MATCH (n:ShouldNotBeThere) RETURN count(n)").single().get(0)) + .asLong(); assertThat(cnt).isEqualTo(0L); } } interface SomeRepository extends Neo4jRepository { - @Transactional // The annotation on the Neo4jRepository is not inherited on the derived methods + @Transactional // The annotation on the Neo4jRepository is not inherited on the + // derived methods @Query("Kaputt") Person broken(); + } @Configuration @@ -94,31 +94,36 @@ class Neo4jTransactionManagerTestIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java index 11609b2d6..c246ab2f1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -37,11 +34,11 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -53,11 +50,15 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; 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.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Gerrit Meier */ @@ -79,11 +80,11 @@ class OptimisticLockingIT { @BeforeEach void setup() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); Transaction tx = session.beginTransaction()) { tx.run("MATCH (n) detach delete n"); tx.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -152,7 +153,7 @@ class OptimisticLockingIT { versionedThings.get(0).setMyVersion(1L); // Version in DB is 0 assertThatExceptionOfType(OptimisticLockingFailureException.class) - .isThrownBy(() -> repository.saveAll(versionedThings)); + .isThrownBy(() -> repository.saveAll(versionedThings)); } @@ -223,22 +224,24 @@ class OptimisticLockingIT { versionedThings.get(0).setMyVersion(1L); // Version in DB is 0 assertThatExceptionOfType(OptimisticLockingFailureException.class) - .isThrownBy(() -> repository.saveAll(versionedThings)); + .isThrownBy(() -> repository.saveAll(versionedThings)); } @Test void shouldNotFailOnDeleteByIdWithNullVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } repository.deleteById(1L); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount").single() - .get("vCount").asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount") + .single() + .get("vCount") + .asLong(); assertThat(count).isEqualTo(0); } @@ -246,17 +249,19 @@ class OptimisticLockingIT { @Test void shouldNotFailOnDeleteByEntityWithNullVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } VersionedThingWithAssignedId thing = repository.findById(1L).get(); repository.delete(thing); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount").single() - .get("vCount").asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount") + .single() + .get("vCount") + .asLong(); assertThat(count).isEqualTo(0); } @@ -264,16 +269,18 @@ class OptimisticLockingIT { @Test void shouldNotFailOnDeleteByIdWithAnyVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1, myVersion:3})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } repository.deleteById(1L); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount").single() - .get("vCount").asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount") + .single() + .get("vCount") + .asLong(); assertThat(count).isEqualTo(0); } @@ -281,9 +288,9 @@ class OptimisticLockingIT { @Test void shouldFailOnDeleteByEntityWithWrongVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1, myVersion:2})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } VersionedThingWithAssignedId thing = repository.findById(1L).get(); @@ -318,10 +325,10 @@ class OptimisticLockingIT { thing2.setOtherVersionedThings(Collections.singletonList(thing1)); repository.save(thing2); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { List result = session - .run("MATCH (t:VersionedThing{name:'Thing1'})-[:HAS]->(:VersionedThing{name:'Thing2'}) return t") - .list(); + .run("MATCH (t:VersionedThing{name:'Thing1'})-[:HAS]->(:VersionedThing{name:'Thing2'}) return t") + .list(); assertThat(result).hasSize(1); } } @@ -351,38 +358,32 @@ class OptimisticLockingIT { // adds // Thing3-[:HAS]->Thing1 - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Long relationshipCount = session - .run("MATCH (:VersionedThing)-[r:HAS]->(:VersionedThing) return count(r) as relationshipCount") - .single().get("relationshipCount").asLong(); + .run("MATCH (:VersionedThing)-[r:HAS]->(:VersionedThing) return count(r) as relationshipCount") + .single() + .get("relationshipCount") + .asLong(); assertThat(relationshipCount).isEqualTo(4); } } @Test // GH-2259 @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) - void shouldLockConcurrentOnAssignedId(@Autowired TransactionTemplate transactionTemplate, @Autowired VersionedThingWithAssignedIdRepository repo) - throws Exception { + void shouldLockConcurrentOnAssignedId(@Autowired TransactionTemplate transactionTemplate, + @Autowired VersionedThingWithAssignedIdRepository repo) throws Exception { - assertVersionLock( - transactionTemplate, - () -> repo.save(new VersionedThingWithAssignedId(1L, "a")), - repo::save, - "MERGE (n:VersionedThingWithAssignedId {id: 1}) ON MATCH SET n.version = 23 RETURN n" - ); + assertVersionLock(transactionTemplate, () -> repo.save(new VersionedThingWithAssignedId(1L, "a")), repo::save, + "MERGE (n:VersionedThingWithAssignedId {id: 1}) ON MATCH SET n.version = 23 RETURN n"); } @Test // GH-2259 @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) - void shouldLockConcurrentOnGeneratedId(@Autowired TransactionTemplate transactionTemplate, @Autowired VersionedThingRepository repo) - throws Exception { + void shouldLockConcurrentOnGeneratedId(@Autowired TransactionTemplate transactionTemplate, + @Autowired VersionedThingRepository repo) throws Exception { - assertVersionLock( - transactionTemplate, - () -> repo.save(new VersionedThing("a")), - repo::save, - "MERGE (n:VersionedThing {name: 'a'}) ON MATCH SET n.version = 23 RETURN n" - ); + assertVersionLock(transactionTemplate, () -> repo.save(new VersionedThing("a")), repo::save, + "MERGE (n:VersionedThing {name: 'a'}) ON MATCH SET n.version = 23 RETURN n"); } private void assertVersionLock(TransactionTemplate transactionTemplate, Callable createInitialEntity, @@ -398,30 +399,37 @@ class OptimisticLockingIT { latch.countDown(); // Trigger the match below but keep tx running try { Thread.sleep(sleep); - } catch (InterruptedException e) { + } + catch (InterruptedException ex) { Thread.currentThread().interrupt(); } }); }); latch.await(); // Wait until the above thread sleeps - assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> updateEntity.accept(entity)); + assertThatExceptionOfType(OptimisticLockingFailureException.class) + .isThrownBy(() -> updateEntity.accept(entity)); boolean timedOut = false; try { executorService.submit(() -> { - try (Session session = driver.session()) { + try (Session session = this.driver.session()) { return session.executeWrite(tx -> tx.run(blockedUpdate).single().get(0).asNode().elementId()); } }).get(sleep / 2, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { + } + catch (TimeoutException ex) { timedOut = true; } assertThat(timedOut).isTrue(); } - interface VersionedThingRepository extends Neo4jRepository {} + interface VersionedThingRepository extends Neo4jRepository { - interface VersionedThingWithAssignedIdRepository extends Neo4jRepository {} + } + + interface VersionedThingWithAssignedIdRepository extends Neo4jRepository { + + } @Configuration @EnableNeo4jRepositories(considerNestedRepositories = true) @@ -429,30 +437,35 @@ class OptimisticLockingIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java index 67fa0f34b..6dfe019a7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; @@ -38,6 +36,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; import org.neo4j.driver.types.MapAccessor; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -47,11 +46,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.Neo4jOperations; -import org.springframework.data.neo4j.integration.shared.common.DoritoEatingPerson; -import org.springframework.data.neo4j.integration.shared.common.GH2621Domain; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -59,6 +55,8 @@ import org.springframework.data.neo4j.integration.issues.gh2451.WidgetEntity; import org.springframework.data.neo4j.integration.issues.gh2451.WidgetProjection; import org.springframework.data.neo4j.integration.issues.gh2451.WidgetRepository; import org.springframework.data.neo4j.integration.shared.common.DepartmentEntity; +import org.springframework.data.neo4j.integration.shared.common.DoritoEatingPerson; +import org.springframework.data.neo4j.integration.shared.common.GH2621Domain; import org.springframework.data.neo4j.integration.shared.common.NamesOnly; import org.springframework.data.neo4j.integration.shared.common.NamesOnlyDto; import org.springframework.data.neo4j.integration.shared.common.NamesWithSpELCity; @@ -76,12 +74,15 @@ import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.repository.support.CypherdslStatementExecutor; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.repository.query.Param; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier * @author Michael J. Simons @@ -90,16 +91,23 @@ import org.springframework.transaction.support.TransactionTemplate; class ProjectionIT { private static final String FIRST_NAME = "Hans"; + private static final String FIRST_NAME2 = "Lieschen"; + private static final String LAST_NAME = "Mueller"; + private static final String CITY = "Braunschweig"; private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; + private final BookmarkCapture bookmarkCapture; + private Long projectionTestRootId; + private Long projectionTest1O1Id; + private Long projectionTestLevel1Id; @Autowired @@ -108,49 +116,61 @@ class ProjectionIT { this.bookmarkCapture = bookmarkCapture; } + private static void projectedEntities(PersonDepartmentQueryResult personAndDepartment) { + assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getId).isEqualTo("p1"); + assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getEmail).isEqualTo("p1@dep1.org"); + assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getId).isEqualTo("d1"); + assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getName).isEqualTo("Dep1"); + } + + private static Statement whoHasFirstName(String firstName) { + Node p = Cypher.node("Person").named("p"); + return Cypher.match(p) + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(firstName))) + .returning(p.getRequiredSymbolicName()) + .build(); + } + @BeforeEach void setup() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction();) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction();) { transaction.run("MATCH (n) detach delete n"); - transaction.run("CREATE (p:PersonEntity {id: 'p1', email: 'p1@dep1.org'}) -[:MEMBER_OF]->(department:DepartmentEntity {id: 'd1', name: 'Dep1'}) RETURN p"); - transaction.run("CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p"); + transaction.run( + "CREATE (p:PersonEntity {id: 'p1', email: 'p1@dep1.org'}) -[:MEMBER_OF]->(department:DepartmentEntity {id: 'd1', name: 'Dep1'}) RETURN p"); + transaction.run( + "CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p"); for (Map.Entry person : new Map.Entry[] { new AbstractMap.SimpleEntry(FIRST_NAME, LAST_NAME), - new AbstractMap.SimpleEntry(FIRST_NAME2, LAST_NAME), - }) { + new AbstractMap.SimpleEntry(FIRST_NAME2, LAST_NAME), }) { transaction.run(" MERGE (address:Address{city: $city})" - + "CREATE (:Person{firstName: $firstName, lastName: $lastName})" - + "-[:LIVES_AT]-> (address)", + + "CREATE (:Person{firstName: $firstName, lastName: $lastName})" + "-[:LIVES_AT]-> (address)", Values.parameters("firstName", person.getKey(), "lastName", person.getValue(), "city", CITY)); } - Record result = transaction.run("create (r:ProjectionTestRoot {name: 'root'}) \n" - + "create (o:ProjectionTest1O1 {name: '1o1'}) " - + "create (l11:ProjectionTestLevel1 {name: 'level11'})\n" - + "create (l12:ProjectionTestLevel1 {name: 'level12'})\n" - + "create (l21:ProjectionTestLevel2 {name: 'level21'})\n" - + "create (l22:ProjectionTestLevel2 {name: 'level22'})\n" - + "create (l23:ProjectionTestLevel2 {name: 'level23'})\n" - + "create (r) - [:ONE_OONE] -> (o)\n" - + "create (r) - [:LEVEL_1] -> (l11)\n" - + "create (r) - [:LEVEL_1] -> (l12)\n" - + "create (l11) - [:LEVEL_2] -> (l21)\n" - + "create (l11) - [:LEVEL_2] -> (l22)\n" - + "create (l12) - [:LEVEL_2] -> (l23)\n" - + "return id(r), id(l11), id(o)").single(); + Record result = transaction + .run("create (r:ProjectionTestRoot {name: 'root'}) \n" + "create (o:ProjectionTest1O1 {name: '1o1'}) " + + "create (l11:ProjectionTestLevel1 {name: 'level11'})\n" + + "create (l12:ProjectionTestLevel1 {name: 'level12'})\n" + + "create (l21:ProjectionTestLevel2 {name: 'level21'})\n" + + "create (l22:ProjectionTestLevel2 {name: 'level22'})\n" + + "create (l23:ProjectionTestLevel2 {name: 'level23'})\n" + "create (r) - [:ONE_OONE] -> (o)\n" + + "create (r) - [:LEVEL_1] -> (l11)\n" + "create (r) - [:LEVEL_1] -> (l12)\n" + + "create (l11) - [:LEVEL_2] -> (l21)\n" + "create (l11) - [:LEVEL_2] -> (l22)\n" + + "create (l12) - [:LEVEL_2] -> (l23)\n" + "return id(r), id(l11), id(o)") + .single(); - projectionTestRootId = result.get(0).asLong(); - projectionTestLevel1Id = result.get(1).asLong(); - projectionTest1O1Id = result.get(2).asLong(); + this.projectionTestRootId = result.get(0).asLong(); + this.projectionTestLevel1Id = result.get(1).asLong(); + this.projectionTest1O1Id = result.get(2).asLong(); transaction.run("create (w:Widget {code: 'Window1', label: 'yyy'})").consume(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -163,7 +183,8 @@ class ProjectionIT { assertThat(people).extracting(NamesOnly::getFirstName).containsExactlyInAnyOrder(FIRST_NAME, FIRST_NAME2); assertThat(people).extracting(NamesOnly::getLastName).containsOnly(LAST_NAME); - assertThat(people).extracting(NamesOnly::getFullName).containsExactlyInAnyOrder(FIRST_NAME + " " + LAST_NAME, FIRST_NAME2 + " " + LAST_NAME); + assertThat(people).extracting(NamesOnly::getFullName) + .containsExactlyInAnyOrder(FIRST_NAME + " " + LAST_NAME, FIRST_NAME2 + " " + LAST_NAME); } @Test // GH-2325 @@ -172,7 +193,8 @@ class ProjectionIT { Collection people = repository.findProjectionByLastName(LAST_NAME); assertThat(people).hasSize(2); - assertThat(people).extracting(NamesWithSpELCity::getFirstName).containsExactlyInAnyOrder(FIRST_NAME, FIRST_NAME2); + assertThat(people).extracting(NamesWithSpELCity::getFirstName) + .containsExactlyInAnyOrder(FIRST_NAME, FIRST_NAME2); assertThat(people).extracting(NamesWithSpELCity::getLastName).containsOnly(LAST_NAME); assertThat(people).extracting(NamesWithSpELCity::getCity).containsExactlyInAnyOrder(CITY, CITY); @@ -236,7 +258,8 @@ class ProjectionIT { @Test void findDynamicProjectionForNamesOnlyDto(@Autowired ProjectionPersonRepository repository) { - Collection people = repository.findByLastNameAndFirstName(LAST_NAME, FIRST_NAME, NamesOnlyDto.class); + Collection people = repository.findByLastNameAndFirstName(LAST_NAME, FIRST_NAME, + NamesOnlyDto.class); assertThat(people).hasSize(1); NamesOnlyDto person = people.iterator().next(); @@ -258,7 +281,8 @@ class ProjectionIT { @Test // GH-2139 void projectionsShouldBeSliceable(@Autowired ProjectionPersonRepository repository) { - Slice people = repository.findSliceProjectedBy(PageRequest.of(1, 1, Sort.by("firstName").descending())); + Slice people = repository + .findSliceProjectedBy(PageRequest.of(1, 1, Sort.by("firstName").descending())); assertThat(people.hasPrevious()).isTrue(); assertThat(people.hasNext()).isFalse(); assertThat(people).hasSize(1); @@ -268,57 +292,59 @@ class ProjectionIT { @Test // GH-2164 void findByIdWithProjectionShouldWork(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository - .findById(projectionTestRootId, SimpleProjection.class); + Optional optionalProjection = repository.findById(this.projectionTestRootId, + SimpleProjection.class); assertThat(optionalProjection).map(SimpleProjection::getName).hasValue("root"); } @Test // GH-2165 void relationshipsShouldBeIncludedInProjections(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository - .findById(projectionTestRootId, SimpleProjectionWithLevelAndLower.class); + Optional optionalProjection = repository.findById(this.projectionTestRootId, + SimpleProjectionWithLevelAndLower.class); assertThat(optionalProjection).hasValueSatisfying(p -> { assertThat(p.getName()).isEqualTo("root"); assertThat(p.getOneOone()).extracting(ProjectionTest1O1::getName).isEqualTo("1o1"); assertThat(p.getLevel1()).hasSize(2); - assertThat(p.getLevel1().stream()).anyMatch(e -> e.getId().equals(projectionTestLevel1Id) && e.getLevel2().size() == 2); + assertThat(p.getLevel1().stream()) + .anyMatch(e -> e.getId().equals(this.projectionTestLevel1Id) && e.getLevel2().size() == 2); }); } @Test // GH-2165 void nested1to1ProjectionsShouldWork(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository - .findById(projectionTestRootId, ProjectedOneToOne.class); + Optional optionalProjection = repository.findById(this.projectionTestRootId, + ProjectedOneToOne.class); assertThat(optionalProjection).hasValueSatisfying(p -> { assertThat(p.getName()).isEqualTo("root"); assertThat(p.getOneOone()).extracting(ProjectedOneToOne.Subprojection::getFullName) - .isEqualTo(projectionTest1O1Id + " 1o1"); + .isEqualTo(this.projectionTest1O1Id + " 1o1"); }); } @Test void nested1to1ProjectionsWithNestedProjectionShouldWork(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository - .findById(projectionTestRootId, ProjectionWithNestedProjection.class); + Optional optionalProjection = repository.findById(this.projectionTestRootId, + ProjectionWithNestedProjection.class); assertThat(optionalProjection).hasValueSatisfying(p -> { assertThat(p.getName()).isEqualTo("root"); assertThat(p.getLevel1()).extracting("name").containsExactlyInAnyOrder("level11", "level12"); - assertThat(p.getLevel1()).flatExtracting("level2").extracting("name") - .containsExactlyInAnyOrder("level21", "level22", "level23"); + assertThat(p.getLevel1()).flatExtracting("level2") + .extracting("name") + .containsExactlyInAnyOrder("level21", "level22", "level23"); }); } @Test // GH-2165 void nested1toManyProjectionsShouldWork(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository - .findById(projectionTestRootId, ProjectedOneToMany.class); + Optional optionalProjection = repository.findById(this.projectionTestRootId, + ProjectedOneToMany.class); assertThat(optionalProjection).hasValueSatisfying(p -> { assertThat(p.getName()).isEqualTo("root"); @@ -329,7 +355,7 @@ class ProjectionIT { @Test // GH-2164 void findByIdInDerivedFinderMethodInRelatedObjectShouldWork(@Autowired TreestructureRepository repository) { - Optional optionalProjection = repository.findOneByLevel1Id(projectionTestLevel1Id); + Optional optionalProjection = repository.findOneByLevel1Id(this.projectionTestLevel1Id); assertThat(optionalProjection).map(ProjectionTestRoot::getName).hasValue("root"); } @@ -337,7 +363,8 @@ class ProjectionIT { void findByIdInDerivedFinderMethodInRelatedObjectWithProjectionShouldWork( @Autowired TreestructureRepository repository) { - Optional optionalProjection = repository.findOneByLevel1Id(projectionTestLevel1Id, SimpleProjection.class); + Optional optionalProjection = repository.findOneByLevel1Id(this.projectionTestLevel1Id, + SimpleProjection.class); assertThat(optionalProjection).map(SimpleProjection::getName).hasValue("root"); } @@ -363,22 +390,18 @@ class ProjectionIT { void projectionsContainingKnownEntitiesShouldWorkFromRepository(@Autowired PersonRepository personRepository) { List results = personRepository.findPersonWithDepartment(); - assertThat(results) - .hasSize(1) - .first() - .satisfies(ProjectionIT::projectedEntities); + assertThat(results).hasSize(1).first().satisfies(ProjectionIT::projectedEntities); } @Test // GH-2349 void projectionsContainingKnownEntitiesShouldWorkFromTemplate(@Autowired Neo4jTemplate template) { - List results = template.find(PersonEntity.class).as(PersonDepartmentQueryResult.class) - .matching("MATCH (person:PersonEntity)-[:MEMBER_OF]->(department:DepartmentEntity) RETURN person, department") - .all(); - assertThat(results) - .hasSize(1) - .first() - .satisfies(ProjectionIT::projectedEntities); + List results = template.find(PersonEntity.class) + .as(PersonDepartmentQueryResult.class) + .matching( + "MATCH (person:PersonEntity)-[:MEMBER_OF]->(department:DepartmentEntity) RETURN person, department") + .all(); + assertThat(results).hasSize(1).first().satisfies(ProjectionIT::projectedEntities); } @Test // GH-2451 @@ -409,8 +432,10 @@ class ProjectionIT { @Test // GH-2371 void saveWithCustomPropertyNameWorks(@Autowired Neo4jTemplate neo4jTemplate) { - PersonWithNoConstructor person = neo4jTemplate.findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(), - PersonWithNoConstructor.class).get(); + PersonWithNoConstructor person = neo4jTemplate + .findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(), + PersonWithNoConstructor.class) + .get(); person.setName("rotnroll666"); person.setFirstName("Michael"); @@ -418,10 +443,8 @@ class ProjectionIT { neo4jTemplate.saveAs(person, ProjectedPersonWithNoConstructor.class); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Record record = session - .run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p") - .single(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + Record record = session.run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p").single(); MapAccessor p = record.get("p").asNode(); assertThat(p.get("first_name").asString()).isEqualTo("Michael"); @@ -430,7 +453,7 @@ class ProjectionIT { } @Test // GH-2578 - public void projectionRespectedWithInexactPropertyNameMatch(@Autowired Neo4jOperations neo4jOperations) { + void projectionRespectedWithInexactPropertyNameMatch(@Autowired Neo4jOperations neo4jOperations) { final DoritoEatingPerson person = new DoritoEatingPerson("Bob"); person.setEatsDoritos(true); person.setFriendsAlsoEatDoritos(true); @@ -446,7 +469,7 @@ class ProjectionIT { } @Test // GH-2578 - public void projectionRespected(@Autowired Neo4jOperations neo4jOperations) { + void projectionRespected(@Autowired Neo4jOperations neo4jOperations) { final DoritoEatingPerson person = new DoritoEatingPerson("Ben"); person.setEatsDoritos(true); person.setFriendsAlsoEatDoritos(true); @@ -462,7 +485,8 @@ class ProjectionIT { } @Test // GH-2621 - public void nestedProjectWithFluentOpsShouldWork(@Autowired TransactionTemplate transactionTemplate, @Autowired Neo4jTemplate neo4jTemplate) { + void nestedProjectWithFluentOpsShouldWork(@Autowired TransactionTemplate transactionTemplate, + @Autowired Neo4jTemplate neo4jTemplate) { GH2621Domain.FooProjection fooProjection = transactionTemplate.execute(tx -> { final GH2621Domain.BarBarProjection barBarProjection = new GH2621Domain.BarBarProjection("v1", "v2"); @@ -471,19 +495,21 @@ class ProjectionIT { assertThat(fooProjection.getBar()).isNotNull(); assertThat(fooProjection.getBar().getValue1()).isEqualTo("v1"); - // There is no way to deduce from a `BarProjection` field the correlation from `BarBarProjection to `BarBar` + // There is no way to deduce from a `BarProjection` field the correlation from + // `BarBarProjection to `BarBar` // without throwing a dice and we are not going to try this assertThat(fooProjection.getBar()).isInstanceOf(GH2621Domain.BarProjection.class); // The result above is reflected in the graph - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Record result = session.run("MATCH (n:GH2621Bar) RETURN n").single(); assertThat(result.get("n").asNode().get("value1").asString()).isEqualTo("v1"); } } @Test // GH-2621 - public void nestedProjectWithFluentOpsShouldWork2(@Autowired TransactionTemplate transactionTemplate, @Autowired Neo4jTemplate neo4jTemplate) { + void nestedProjectWithFluentOpsShouldWork2(@Autowired TransactionTemplate transactionTemplate, + @Autowired Neo4jTemplate neo4jTemplate) { GH2621Domain.FooProjection fooProjection = transactionTemplate.execute(tx -> { GH2621Domain.Foo foo = new GH2621Domain.Foo(new GH2621Domain.BarBar("v1", "v2")); @@ -492,43 +518,33 @@ class ProjectionIT { assertThat(fooProjection.getBar()).isNotNull(); assertThat(fooProjection.getBar().getValue1()).isEqualTo("v1"); - // There is no way to deduce from a `BarProjection` field the correlation from `BarBarProjection to `BarBar` + // There is no way to deduce from a `BarProjection` field the correlation from + // `BarBarProjection to `BarBar` // without throwing a dice and we are not going to try this assertThat(fooProjection.getBar()).isInstanceOf(GH2621Domain.BarProjection.class); // This is a different here as the concrete dto was used during save ops, so the - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Record result = session.run("MATCH (n:GH2621Bar:GH2621BarBar) RETURN n").single(); org.neo4j.driver.types.Node node = result.get("n").asNode(); assertThat(node.get("value1").asString()).isEqualTo("v1"); - // This is a limitation of the Spring Data Commons support for the DTO projections - // when we reach org/springframework/data/neo4j/core/PropertyFilterSupport.java:141 we call - // org.springframework.data.projection.ProjectionFactory.getProjectionInformation and we only - // have the concrete type information at hand, in the domain example FooProjection#bar, which points - // to BarProjection, without any clue that we do want a BarBarProjection being used during saving. - // So with the example in the ticket, saving value2 (or anything on the BarBarProjection) won't + // This is a limitation of the Spring Data Commons support for the DTO + // projections + // when we reach + // org/springframework/data/neo4j/core/PropertyFilterSupport.java:141 we call + // org.springframework.data.projection.ProjectionFactory.getProjectionInformation + // and we only + // have the concrete type information at hand, in the domain example + // FooProjection#bar, which points + // to BarProjection, without any clue that we do want a BarBarProjection being + // used during saving. + // So with the example in the ticket, saving value2 (or anything on the + // BarBarProjection) won't // be possible assertThat(node.get("value2").isNull()).isTrue(); } } - private static void projectedEntities(PersonDepartmentQueryResult personAndDepartment) { - assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getId).isEqualTo("p1"); - assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getEmail).isEqualTo("p1@dep1.org"); - assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getId).isEqualTo("d1"); - assertThat(personAndDepartment.getDepartment()).extracting(DepartmentEntity::getName).isEqualTo("Dep1"); - } - - private static Statement whoHasFirstName(String firstName) { - Node p = Cypher.node("Person").named("p"); - return Cypher.match(p) - .where(p.property("firstName").isEqualTo(Cypher.anonParameter(firstName))) - .returning( - p.getRequiredSymbolicName() - ) - .build(); - } - interface ProjectedPersonWithNoConstructor { String getName(); @@ -536,14 +552,16 @@ class ProjectionIT { String getFirstName(); String getMittlererName(); + } interface PersonWithNoConstructorRepository extends Neo4jRepository { ProjectedPersonWithNoConstructor findByName(String name); + } - interface ProjectionPersonRepository extends Neo4jRepository, CypherdslStatementExecutor { + interface ProjectionPersonRepository extends Neo4jRepository, CypherdslStatementExecutor { Collection findByLastName(String lastName); @@ -561,6 +579,7 @@ class ProjectionIT { Collection findByFirstNameAndLastName(String firstName, String lastName); Collection findByLastNameAndFirstName(String lastName, String firstName, Class projectionClass); + } interface TreestructureRepository extends Neo4jRepository { @@ -570,11 +589,13 @@ class ProjectionIT { Optional findOneByLevel1Id(Long idOfLevel1); Optional findOneByLevel1Id(Long idOfLevel1, Class typeOfProjection); + } interface SimpleProjection { String getName(); + } interface SimpleProjectionWithLevelAndLower { @@ -584,6 +605,7 @@ class ProjectionIT { ProjectionTest1O1 getOneOone(); List getLevel1(); + } interface ProjectedOneToOne { @@ -595,11 +617,14 @@ class ProjectionIT { interface Subprojection { /** - * @return Some arbitrary computed projection result to make sure that machinery works as well + * @return Some arbitrary computed projection result to make sure that + * machinery works as well */ @Value("#{target.id + ' ' + target.name}") String getFullName(); + } + } interface ProjectedOneToMany { @@ -611,11 +636,14 @@ class ProjectionIT { interface Subprojection { /** - * @return Some arbitrary computed projection result to make sure that machinery works as well + * @return Some arbitrary computed projection result to make sure that + * machinery works as well */ @Value("#{target.id + ' ' + target.name}") String getFullName(); + } + } interface ProjectionWithNestedProjection { @@ -625,32 +653,42 @@ class ProjectionIT { List getLevel1(); interface Subprojection1 { + String getName(); + List getLevel2(); + } interface Subprojection2 { + String getName(); + } + } interface PersonRepository extends Neo4jRepository { + @Query("MATCH (person:PersonEntity)-[:MEMBER_OF]->(department:DepartmentEntity) RETURN person, department") List findPersonWithDepartment(); + } @Configuration - @EnableNeo4jRepositories(considerNestedRepositories = true, basePackageClasses = {ProjectionIT.class, WidgetEntity.class}) + @EnableNeo4jRepositories(considerNestedRepositories = true, + basePackageClasses = { ProjectionIT.class, WidgetEntity.class }) @EnableTransactionManagement static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -664,10 +702,12 @@ class ProjectionIT { } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override @@ -679,5 +719,7 @@ class ProjectionIT { TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java index 52cb07cdd..9883b5462 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java @@ -15,18 +15,23 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.Map; import java.util.stream.Stream; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.Expressions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,7 +41,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -45,18 +49,14 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; 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.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import com.querydsl.core.types.Ops; -import com.querydsl.core.types.Order; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.Expressions; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -67,26 +67,27 @@ class QuerydslNeo4jPredicateExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Path personPath; + private final Path firstNamePath; + private final Path lastNamePath; QuerydslNeo4jPredicateExecutorIT() { this.personPath = Expressions.path(Person.class, "person"); - this.firstNamePath = Expressions.path(String.class, personPath, "firstName"); - this.lastNamePath = Expressions.path(String.class, personPath, "lastName"); + this.firstNamePath = Expressions.path(String.class, this.personPath, "firstName"); + this.lastNamePath = Expressions.path(String.class, this.personPath, "lastName"); } @BeforeAll protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -96,7 +97,7 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentFindOneShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); Person person = repository.findBy(predicate, FetchableFluentQuery::oneValue); assertThat(person).isNotNull(); @@ -106,41 +107,38 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); List people = repository.findBy(predicate, FetchableFluentQuery::all); - assertThat(people).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela", "Helge"); + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Bela", "Helge"); } @Test // GH-2343 void fluentFindAllProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); List people = repository.findBy(predicate, q -> q.project("firstName").all()); - assertThat(people) - .hasSize(1) - .first().satisfies(p -> { - assertThat(p.getFirstName()).isEqualTo("Helge"); - assertThat(p.getId()).isNotNull(); + assertThat(people).hasSize(1).first().satisfies(p -> { + assertThat(p.getFirstName()).isEqualTo("Helge"); + assertThat(p.getId()).isNotNull(); - assertThat(p.getLastName()).isNull(); - assertThat(p.getAddress()).isNull(); - }); + assertThat(p.getLastName()).isNull(); + assertThat(p.getAddress()).isNull(); + }); } @Test @Tag("GH-2726") void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); - Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset())); + Window peopleWindow = repository.findBy(predicate, + q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset())); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Helge"); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactlyInAnyOrder("Helge"); assertThat(peopleWindow.isLast()).isFalse(); assertThat(peopleWindow.hasNext()).isTrue(); @@ -151,13 +149,13 @@ class QuerydslNeo4jPredicateExecutorIT { @Test @Tag("GH-2726") void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); - Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))); + Window peopleWindow = repository.findBy(predicate, + q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela"); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactlyInAnyOrder("Bela"); assertThat(peopleWindow.isLast()).isTrue(); @@ -167,16 +165,16 @@ class QuerydslNeo4jPredicateExecutorIT { @Test @Tag("GH-2726") void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); var firstName = Sort.by("firstName").descending(); - Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).sortBy(firstName).scroll(ScrollPosition.offset())); + Window peopleWindow = repository.findBy(predicate, + q -> q.limit(1).sortBy(firstName).scroll(ScrollPosition.offset())); ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0)); peopleWindow = repository.findBy(predicate, q -> q.limit(1).sortBy(firstName).scroll(currentPosition)); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela"); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactlyInAnyOrder("Bela"); assertThat(peopleWindow.isLast()).isTrue(); } @@ -184,18 +182,17 @@ class QuerydslNeo4jPredicateExecutorIT { @Test @Tag("GH-2726") void scrollByExampleWithKeysetOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); - Window peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(ScrollPosition.keyset())); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactly("Bela"); + Window peopleWindow = repository.findBy(predicate, + q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(ScrollPosition.keyset())); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactly("Bela"); ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.size() - 1); peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Helge"); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactlyInAnyOrder("Helge"); assertThat(peopleWindow.isLast()).isTrue(); } @@ -203,54 +200,34 @@ class QuerydslNeo4jPredicateExecutorIT { @Test @Tag("GH-2726") void scrollByExampleWithKeysetOffsetBackward(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); - KeysetScrollPosition startPosition = ScrollPosition.backward(Map.of( - "lastName", "Schneider" - )); - Window peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(startPosition)); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactly("Helge"); + KeysetScrollPosition startPosition = ScrollPosition.backward(Map.of("lastName", "Schneider")); + Window peopleWindow = repository.findBy(predicate, + q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(startPosition)); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactly("Helge"); - var nextPos = ScrollPosition.backward( - ((KeysetScrollPosition) peopleWindow.positionAt(0)).getKeys()); + var nextPos = ScrollPosition.backward(((KeysetScrollPosition) peopleWindow.positionAt(0)).getKeys()); peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(nextPos)); - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela"); - } - - static class DtoPersonProjection { - - private final String firstName; - - DtoPersonProjection(String firstName) { - this.firstName = firstName; - } - - public String getFirstName() { - return firstName; - } + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName).containsExactlyInAnyOrder("Bela"); } @Test // GH-2343 void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); List people = repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()); - assertThat(people) - .hasSize(1) - .extracting(DtoPersonProjection::getFirstName) - .first().isEqualTo("Helge"); + assertThat(people).hasSize(1).extracting(DtoPersonProjection::getFirstName).first().isEqualTo("Helge"); } @Test // GH-2343 void fluentStreamShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); Stream people = repository.findBy(predicate, FetchableFluentQuery::stream); assertThat(people.map(Person::getFirstName)).containsExactly("Helge"); @@ -259,7 +236,7 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentStreamProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); Stream people = repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).stream()); @@ -270,7 +247,8 @@ class QuerydslNeo4jPredicateExecutorIT { void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { Predicate predicate = Expressions.TRUE.isTrue(); - Person person = repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).firstValue()); + Person person = repository.findBy(predicate, + q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).firstValue()); assertThat(person).isNotNull(); assertThat(person).extracting(Person::getFirstName).isEqualTo("Helge"); @@ -289,8 +267,8 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); Page people = repository.findBy(predicate, q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))); @@ -302,7 +280,7 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); boolean exists = repository.findBy(predicate, q -> q.exists()); assertThat(exists).isTrue(); @@ -311,8 +289,8 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2343 void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); long count = repository.findBy(predicate, q -> q.count()); assertThat(count).isEqualTo(2); @@ -321,10 +299,10 @@ class QuerydslNeo4jPredicateExecutorIT { @Test // GH-2726 void fluentFindAllWithLimitShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - List people = repository.findBy(predicate, - q -> q.sortBy(Sort.by("firstName").descending()).limit(1)).all(); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + List people = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName").descending()).limit(1)) + .all(); assertThat(people).hasSize(1); assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); @@ -333,104 +311,111 @@ class QuerydslNeo4jPredicateExecutorIT { @Test void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findOne(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")))) - .hasValueSatisfying(p -> assertThat(p).extracting(Person::getLastName).isEqualTo("Schneider")); + assertThat(repository.findOne(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")))) + .hasValueSatisfying(p -> assertThat(p).extracting(Person::getLastName).isEqualTo("Schneider")); } @Test void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))))) - .extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela", "Helge"); + assertThat(repository.findAll(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))))) + .extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela", "Helge"); } @Test void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat( - repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - new OrderSpecifier(Order.DESC, lastNamePath) - )) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(repository.findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + new OrderSpecifier(Order.DESC, this.lastNamePath))) + .extracting(Person::getFirstName) + .containsExactly("Helge", "Bela"); } @Test void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat( - repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - Sort.by("lastName").descending() - )) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(repository.findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + Sort.by("lastName").descending())) + .extracting(Person::getFirstName) + .containsExactly("Helge", "Bela"); } @Test void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findAll(new OrderSpecifier(Order.DESC, lastNamePath))) - .extracting(Person::getFirstName) - .containsExactly("Helge", "B", "A", "Bela"); + assertThat(repository.findAll(new OrderSpecifier(Order.DESC, this.lastNamePath))) + .extracting(Person::getFirstName) + .containsExactly("Helge", "B", "A", "Bela"); } @Test void pagedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - PageRequest.of(1, 1, Sort.by("lastName").descending()) - ); + Page people = repository.findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + PageRequest.of(1, 1, Sort.by("lastName").descending())); assertThat(people.hasPrevious()).isTrue(); assertThat(people.hasNext()).isFalse(); assertThat(people.getTotalElements()).isEqualTo(2); - assertThat(people) - .extracting(Person::getFirstName) - .containsExactly("Bela"); + assertThat(people).extracting(Person::getFirstName).containsExactly("Bela"); } @Test // GH-2194 void pagedFindAllShouldWork2(@Autowired QueryDSLPersonRepository repository) { - Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - PageRequest.of(0, 20, Sort.by("lastName").descending()) - ); + Page people = repository.findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + PageRequest.of(0, 20, Sort.by("lastName").descending())); assertThat(people.hasPrevious()).isFalse(); assertThat(people.hasNext()).isFalse(); assertThat(people.getTotalElements()).isEqualTo(2); - assertThat(people) - .extracting(Person::getFirstName) - .containsExactly("Helge", "Bela"); + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge", "Bela"); } @Test void countShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat( - repository.count(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))) - )) - .isEqualTo(2L); + assertThat(repository.count(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))))).isEqualTo(2L); } @Test void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.exists(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("A")))) - .isTrue(); + assertThat(repository.exists(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("A")))) + .isTrue(); } // tag::sdn-mixins.dynamic-conditions.add-mixin[] - interface QueryDSLPersonRepository extends - Neo4jRepository, // <.> - QuerydslPredicateExecutor { // <.> + interface QueryDSLPersonRepository extends Neo4jRepository, // <.> + QuerydslPredicateExecutor { + + // <.> + + } + + static class DtoPersonProjection { + + private final String firstName; + + DtoPersonProjection(String firstName) { + this.firstName = firstName; + } + + String getFirstName() { + return this.firstName; + } + } // end::sdn-mixins.dynamic-conditions.add-mixin[] @@ -440,26 +425,31 @@ class QuerydslNeo4jPredicateExecutorIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsAsConstructorParametersIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsAsConstructorParametersIT.java index f9144bfb3..01553f06d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsAsConstructorParametersIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsAsConstructorParametersIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -24,10 +22,10 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -35,16 +33,20 @@ import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.common.RelationshipsAsConstructorParametersEntities; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + @Neo4jIntegrationTest class RelationshipsAsConstructorParametersIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; protected final Driver driver; + private final BookmarkCapture bookmarkCapture; @Autowired @@ -56,31 +58,33 @@ class RelationshipsAsConstructorParametersIT { @BeforeEach protected void setupData() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n").consume(); - transaction - .run("CREATE (b:NodeTypeB {name: 'detail'}) - [:BELONGS_TO] -> (a:NodeTypeA {name: 'master'}) RETURN a, b") - .consume(); + transaction.run( + "CREATE (b:NodeTypeB {name: 'detail'}) - [:BELONGS_TO] -> (a:NodeTypeA {name: 'master'}) RETURN a, b") + .consume(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } /** - * Partially immutable entity with association filled during construction. Failed originally due to the fact that we - * did not check if the association was a constructor property. - * + * Partially immutable entity with association filled during construction. Failed + * originally due to the fact that we did not check if the association was a + * constructor property. * @param template Needed for executing the query. */ @Test void shouldCreateMasterDetailRelationshipViaConstructor(@Autowired Neo4jTemplate template) { List details = template - .findAll(RelationshipsAsConstructorParametersEntities.NodeTypeB.class); + .findAll(RelationshipsAsConstructorParametersEntities.NodeTypeB.class); assertThat(details).hasSize(1).element(0).satisfies(content -> { assertThat(content.getName()).isEqualTo("detail"); assertThat(content.getNodeTypeA()).isNotNull() - .extracting(RelationshipsAsConstructorParametersEntities.NodeTypeA::getName).isEqualTo("master"); + .extracting(RelationshipsAsConstructorParametersEntities.NodeTypeA::getName) + .isEqualTo("master"); }); } @@ -89,25 +93,30 @@ class RelationshipsAsConstructorParametersIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsIT.java index 75a58b511..674d7cd14 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RelationshipsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import java.util.List; import java.util.Optional; @@ -27,10 +25,10 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -39,12 +37,16 @@ import org.springframework.data.neo4j.integration.shared.common.MultipleRelation import org.springframework.data.neo4j.integration.shared.common.RelationshipsITBase; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** - * Test cases for various relationship scenarios (self references, multiple times to same instance). + * Test cases for various relationship scenarios (self references, multiple times to same + * instance). * * @author Michael J. Simons */ @@ -64,12 +66,14 @@ class RelationshipsIT extends RelationshipsITBase { p = repository.save(p); Optional loadedThing = repository.findById(p.getId()); - assertThat(loadedThing).isPresent().map(MultipleRelationshipsThing::getTypeA) - .map(MultipleRelationshipsThing::getName).hasValue("c"); + assertThat(loadedThing).isPresent() + .map(MultipleRelationshipsThing::getTypeA) + .map(MultipleRelationshipsThing::getName) + .hasValue("c"); - try (Session session = driver.session()) { + try (Session session = this.driver.session()) { List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") - .list(r -> r.get("name").asString()); + .list(r -> r.get("name").asString()); assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); } } @@ -83,24 +87,25 @@ class RelationshipsIT extends RelationshipsITBase { p = repository.save(p); Optional loadedThing = repository.findById(p.getId()); - assertThat(loadedThing).isPresent().map(MultipleRelationshipsThing::getTypeB) - .hasValueSatisfying(l -> assertThat(l).extracting(MultipleRelationshipsThing::getName).containsExactly("c")); + assertThat(loadedThing).isPresent() + .map(MultipleRelationshipsThing::getTypeB) + .hasValueSatisfying( + l -> assertThat(l).extracting(MultipleRelationshipsThing::getName).containsExactly("c")); - try (Session session = driver.session()) { + try (Session session = this.driver.session()) { List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") - .list(r -> r.get("name").asString()); + .list(r -> r.get("name").asString()); assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); } } /** * This stores multiple, different instances. - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameObjectType(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); p.setTypeA(new MultipleRelationshipsThing("c1")); @@ -122,27 +127,26 @@ class RelationshipsIT extends RelationshipsITBase { assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c3"); }); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session - .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") - .list(record -> { - String type = record.get("r").asRelationship().type(); - String name = record.get("o").get("name").asString(); - return type + "_" + name; - }); + .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c2", "TYPE_C_c3"); } } /** * This stores the same instance in different relationships - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameInstance(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); @@ -165,28 +169,26 @@ class RelationshipsIT extends RelationshipsITBase { assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); }); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session - .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") - .list(record -> { - String type = record.get("r").asRelationship().type(); - String name = record.get("o").get("name").asString(); - return type + "_" + name; - }); + .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); } } /** * This stores the same instance in different relationships - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameInstanceWithBackReference( - @Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired MultipleRelationshipsThingRepository repository, @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); @@ -211,7 +213,7 @@ class RelationshipsIT extends RelationshipsITBase { assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); }); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Function withMapper = record -> { String type = record.get("r").asRelationship().type(); @@ -230,39 +232,36 @@ class RelationshipsIT extends RelationshipsITBase { @Test // DATAGRAPH-1424 void shouldMatchOnTheCorrectRelationship(@Autowired Multiple1O1RelationshipsRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig()); Transaction tx = session.beginTransaction()) { - tx.run("" - + "CREATE (p1:AltPerson {name: 'val1'})\n" - + "CREATE (p2:AltPerson {name: 'val2'})\n" - + "CREATE (p3:AltPerson {name: 'val3'})\n" - + "CREATE (m1:Multiple1O1Relationships {name: 'm1'})\n" - + "CREATE (m2:Multiple1O1Relationships {name: 'm2'})\n" - + "CREATE (m1) - [:REL_1] -> (p1)\n" - + "CREATE (m1) - [:REL_2] -> (p2)\n" - + "CREATE (m2) - [:REL_1] -> (p1)\n" - + "CREATE (m2) - [:REL_2] -> (p3)"); + tx.run("" + "CREATE (p1:AltPerson {name: 'val1'})\n" + "CREATE (p2:AltPerson {name: 'val2'})\n" + + "CREATE (p3:AltPerson {name: 'val3'})\n" + "CREATE (m1:Multiple1O1Relationships {name: 'm1'})\n" + + "CREATE (m2:Multiple1O1Relationships {name: 'm2'})\n" + "CREATE (m1) - [:REL_1] -> (p1)\n" + + "CREATE (m1) - [:REL_2] -> (p2)\n" + "CREATE (m2) - [:REL_1] -> (p1)\n" + + "CREATE (m2) - [:REL_2] -> (p3)"); tx.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); } List objects = repository.findAllByPerson1NameAndPerson2Name("val1", "val2"); - assertThat(objects).hasSize(1).first() - .satisfies(m -> { - assertThat(m.getName()).isEqualTo("m1"); - assertThat(m.getPerson1().getName()).isEqualTo("val1"); - assertThat(m.getPerson2().getName()).isEqualTo("val2"); - }); + assertThat(objects).hasSize(1).first().satisfies(m -> { + assertThat(m.getName()).isEqualTo("m1"); + assertThat(m.getPerson1().getName()).isEqualTo("val1"); + assertThat(m.getPerson2().getName()).isEqualTo("val2"); + }); } - interface MultipleRelationshipsThingRepository extends CrudRepository {} + interface MultipleRelationshipsThingRepository extends CrudRepository { + + } interface Multiple1O1RelationshipsRepository extends CrudRepository { List findAllByPerson1NameAndPerson2Name(String name1, String name2); + } @Configuration @@ -271,25 +270,30 @@ class RelationshipsIT extends RelationshipsITBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index dccfd568d..f74ff1007 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.tuple; - import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -69,6 +63,7 @@ import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; import org.neo4j.driver.types.Relationship; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -176,6 +171,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; + /** * @author Michael J. Simons * @author Gerrit Meier @@ -184,70 +185,567 @@ import org.springframework.transaction.support.TransactionTemplate; */ @ExtendWith(Neo4jExtension.class) @SpringJUnitConfig -@DirtiesContext // We need this here as the nested tests all inherit from the integration test base but the database selection here is different +@DirtiesContext // We need this here as the nested tests all inherit from the integration + // test base but the database selection here is different class RepositoryIT { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - protected static final ThreadLocal databaseSelection = ThreadLocal.withInitial(DatabaseSelection::undecided); - protected static final ThreadLocal userSelection = ThreadLocal.withInitial(UserSelection::connectedUser); + protected static final ThreadLocal databaseSelection = ThreadLocal + .withInitial(DatabaseSelection::undecided); + + protected static final ThreadLocal userSelection = ThreadLocal + .withInitial(UserSelection::connectedUser); private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; + private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; + private static final String TEST_PERSON2_FIRST_NAME = "Bert"; + private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); + private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); + private static final String TEST_PERSON_SAMEVALUE = "SameValue"; + private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint(); + private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint(); + private static final Point CLARION = Values.point(4326, 12.994243, 55.607726).asPoint(); + private static final Point MINC = Values.point(4326, 12.994039, 55.611496).asPoint(); + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + Long id1; + + Long id2; + + PersonWithAllConstructor person1; + + PersonWithAllConstructor person2; + static PersonWithAllConstructor personExample(String sameValue) { return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null, null); } - Long id1; - Long id2; - PersonWithAllConstructor person1; - PersonWithAllConstructor person2; + @Test // GH-2706 + void findByOffsetDateTimeShouldWork(@Autowired TemporalRepository temporalRepository) { + + temporalRepository.deleteAll(); + + LocalDateTime fixedDateTime = LocalDateTime.of(2023, 1, 1, 21, 21, 0); + ZoneId europeBerlin = TimeZone.getTimeZone("Europe/Berlin").toZoneId(); + OffsetDateTime v1 = OffsetDateTime.of(fixedDateTime, europeBerlin.getRules().getOffset(fixedDateTime)); + LocalTime v2 = fixedDateTime.toLocalTime(); + + temporalRepository.save(new OffsetTemporalEntity(v1, v2)); + temporalRepository.save(new OffsetTemporalEntity(v1.minusDays(2), v2.minusMinutes(2))); + + assertThat(temporalRepository.findAllByProperty1After(v1)).isEmpty(); + assertThat(temporalRepository.findAllByProperty2After(v2)).isEmpty(); + + assertThat(temporalRepository.findAllByProperty1After(v1.minusDays(1))).hasSize(1); + assertThat(temporalRepository.findAllByProperty2After(v2.minusMinutes(1))).hasSize(1); + } + + interface BidirectionalExternallyGeneratedIdRepository + extends Neo4jRepository { + + } + + interface BidirectionalAssignedIdRepository extends Neo4jRepository { + + } + + interface BidirectionalStartRepository extends Neo4jRepository { + + } + + interface BidirectionalEndRepository extends Neo4jRepository { + + } + + interface LoopingRelationshipRepository extends Neo4jRepository { + + } + + interface ImmutablePersonRepository extends Neo4jRepository { + + } + + interface MultipleLabelRepository extends Neo4jRepository { + + } + + interface MultipleLabelWithAssignedIdRepository + extends Neo4jRepository { + + } + + interface PersonWithRelationshipWithPropertiesRepository + extends Neo4jRepository { + + @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") + PersonWithRelationshipWithProperties loadFromCustomQuery(@Param("id") Long id); + + PersonWithRelationshipWithProperties findByHobbiesSince(int since); + + PersonWithRelationshipWithProperties findByHobbiesSinceOrHobbiesActive(int since1, boolean active); + + PersonWithRelationshipWithProperties findByHobbiesSinceAndHobbiesActive(int since1, boolean active); + + PersonWithRelationshipWithProperties findByHobbiesHobbyName(String hobbyName); + + @Query("MATCH (p:PersonWithRelationshipWithProperties) return p {.name}") + PersonWithRelationshipWithProperties justTheNames(); + + } + + interface PetRepository extends Neo4jRepository { + + @Query("MATCH (p:Pet)-[r1:Has]->(p2:Pet)-[r2:Has]->(p3:Pet) " + + "where id(p) = $petNode1Id return p, collect(r1), collect(p2), collect(r2), collect(p3)") + Pet customQueryWithDeepRelationshipMapping(@Param("petNode1Id") long petNode1Id); + + @Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)") + Page pagedPets(Pageable pageable); + + @Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)") + Slice slicedPets(Pageable pageable); + + @Query(value = "MATCH (p:#{#staticLabels}) where p.name=$petName return p SKIP $skip LIMIT $limit", + countQuery = "MATCH (p:#{#staticLabels}) return count(p)") + Page pagedPetsWithParameter(@Param("petName") String petName, Pageable pageable); + + Pet findByFriendsName(String friendName); + + Long deleteByNameAndFriendsName(String name, String friendsName); + + Pet findByFriendsFriendsName(String friendName); + + long countByName(String name); + + @Query(value = "RETURN size($0)", count = true) + long countAllByName(String name); + + long countByFriendsNameAndFriendsFriendsName(String friendName, String friendFriendName); + + boolean existsByName(String name); + + @Query("MATCH (n:Pet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:Pet) return n, collect(r), collect(m)") + List findLunas(); + + @Query("MATCH (p:Pet)" + " OPTIONAL MATCH (p)-[rel:Has]->(op)" + " RETURN p, collect(rel), collect(op)") + List findAllFriends(); + + } + + interface ImmutablePetRepository extends Neo4jRepository { + + @Query("MATCH (n:ImmutablePet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:ImmutablePet) return n, collect(r), collect(m)") + List findLunas(); + + } + + interface OneToOneRepository extends Neo4jRepository { + + @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, collect(r), collect(p2)") + List findAllWithCustomQuery(); + + @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, r, p2") + List findAllWithCustomQueryNoCollect(); + + @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) WHERE p1.name = $0 return p1, r, p2") + Optional findOneByName(String name); + + @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return *") + List findAllWithCustomQueryReturnStar(); + + @Query("MATCH (p1:#{#staticLabels}) OPTIONAL MATCH (p1)-[r:OWNS]->(p2:OneToOneTarget) return p1, r, p2") + List findAllWithNullValues(); + + } + + interface RelationshipRepository extends Neo4jRepository { + + @Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) " + + "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets " + + "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) " + + "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies") + PersonWithRelationship getPersonWithRelationshipsViaQuery(); + + @Query("MATCH p=(n:PersonWithRelationship{name:'Freddie'})-[:Has*]->(something) " + + "return n, collect(relationships(p)), collect(nodes(p))") + PersonWithRelationship getPersonWithRelationshipsViaPathQuery(); + + PersonWithRelationship findByPetsName(String petName); + + PersonWithRelationship findByName(String name); + + Page findByName(String name, Pageable pageable); + + List findByName(String name, Sort sort); + + PersonWithRelationship findByHobbiesNameOrPetsName(String hobbyName, String petName); + + PersonWithRelationship findByHobbiesNameAndPetsName(String hobbyName, String petName); + + PersonWithRelationship findByPetsHobbiesName(String hobbyName); + + PersonWithRelationship findByPetsFriendsName(String petName); + + @Transactional + @Query("CREATE (n:PersonWithRelationship) \n" + "SET n.name = $0.__properties__.name \n" + + "WITH n, id(n) as parentId\n" + "UNWIND $0.__properties__.Has as x\n" + "CALL { WITH x, parentId\n" + + " \n" + " WITH x, parentId\n" + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Pet'\n" + + " CREATE (p:Pet {name: x.__properties__.name}) <- [r:Has] - (_)\n" + " RETURN p, r\n" + " \n" + + " UNION\n" + " WITH x, parentId\n" + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Hobby'\n" + + " CREATE (p:Hobby {name: x.__properties__.name}) <- [r:Has] - (_)\n" + " RETURN p, r\n" + "\n" + + " UNION\n" + " WITH x, parentId\n" + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Club'\n" + + " CREATE (p:Club {name: x.__properties__.name}) - [r:Has] -> (_)\n" + " RETURN p, r\n" + "\n" + "}\n" + + "RETURN n, collect(r), collect(p)") + PersonWithRelationship createWithCustomQuery(PersonWithRelationship p); + + @Transactional + @Query("UNWIND $0 AS pwr WITH pwr CREATE (n:PersonWithRelationship) \n" + + "SET n.name = pwr.__properties__.name \n" + "WITH pwr, n, id(n) as parentId\n" + + "UNWIND pwr.__properties__.Has as x\n" + "CALL { WITH x, parentId\n" + " \n" + " WITH x, parentId\n" + + " MATCH (_) \n" + " WHERE id(_) = parentId AND x.__labels__[0] = 'Pet'\n" + + " CREATE (p:Pet {name: x.__properties__.name}) <- [r:Has] - (_)\n" + " RETURN p, r\n" + " \n" + + " UNION\n" + " WITH x, parentId\n" + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Hobby'\n" + + " CREATE (p:Hobby {name: x.__properties__.name}) <- [r:Has] - (_)\n" + " RETURN p, r\n" + "\n" + + " UNION\n" + " WITH x, parentId\n" + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Club'\n" + + " CREATE (p:Club {name: x.__properties__.name}) - [r:Has] -> (_)\n" + " RETURN p, r\n" + "\n" + "}\n" + + "RETURN n, collect(r), collect(p)") + List createManyWithCustomQuery(Collection p); + + PersonWithRelationship.PersonWithHobby findDistinctByHobbiesName(String hobbyName); + + } + + interface SimilarThingRepository extends Neo4jRepository { + + } + + interface BaseClassRepository extends Neo4jRepository { + + @Query("MATCH (n::#{literal(#label)}) RETURN n") + List findByLabel(@Param("label") String label); + + @Query("MATCH (n::#{anyOf(#label)}) RETURN n") + List findByOrLabels(@Param("label") List labels); + + @Query("MATCH (n::#{allOf(#label)}) RETURN n") + List findByAndLabels(@Param("label") Object labels); + + } + + interface SuperBaseClassRepository extends Neo4jRepository { + + @Query("MATCH (n:SuperBaseClass) return n") + List getAllConcreteTypes(); + + } + + interface RelationshipToAbstractClassRepository + extends Neo4jRepository { + + @Query("MATCH (n:RelationshipToAbstractClass)-[h:HAS]->(m:SuperBaseClass) return n, collect(h), collect(m)") + Inheritance.RelationshipToAbstractClass getAllConcreteRelationships(); + + } + + interface BaseClassWithRelationshipRepository extends Neo4jRepository { + + } + + interface SuperBaseClassWithRelationshipRepository + extends Neo4jRepository { + + } + + interface BaseClassWithRelationshipPropertiesRepository + extends Neo4jRepository { + + } + + interface SuperBaseClassWithRelationshipPropertiesRepository + extends Neo4jRepository { + + @Query("MATCH (n:SuperBaseClassWithRelationshipProperties)" + "-[h:HAS]->" + + "(m:SuperBaseClass) return n, collect(h), collect(m)") + List getAllWithHasRelationships(); + + } + + interface BaseClassWithLabelsRepository extends Neo4jRepository { + + } + + interface EntityWithConvertedIdRepository + extends Neo4jRepository { + + } + + interface HobbyWithRelationshipWithPropertiesRepository extends Neo4jRepository { + + @Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)") + AltHobby loadFromCustomQuery(@Param("personId") Long personId); + + } + + interface FriendRepository extends Neo4jRepository { + + } + + interface KotlinPersonRepository extends Neo4jRepository { + + @Query("MATCH (n:KotlinPerson)-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") + List getAllKotlinPersonsViaQuery(); + + @Query("MATCH (n:KotlinPerson{name:'Test'})-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") + KotlinPerson getOneKotlinPersonViaQuery(); + + @Query("MATCH (n:KotlinPerson{name:'Test'})-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") + Optional getOptionalKotlinPersonViaQuery(); + + } + + interface ParentRepository extends Neo4jRepository { + + /** + * Ensure things can be found by base attribute. + * @param someAttribute Base attribute + * @return optional entity + */ + Optional findExtendedParentNodeBySomeAttribute(String someAttribute); + + /** + * Ensure things can be found by extended attribute. + * @param someOtherAttribute Base attribute + * @return optional entity + */ + Optional findExtendedParentNodeBySomeOtherAttribute(String someOtherAttribute); + + } + + interface SimpleEntityWithRelationshipARepository extends Neo4jRepository { + + } + + interface ThingWithFixedGeneratedIdRepository extends Neo4jRepository { + + } + + interface EntityWithRelationshipPropertiesPathRepository + extends Neo4jRepository { + + } + + interface BidirectionalSameEntityRepository extends Neo4jRepository { + + } + + interface SameIdEntitiesWithRelationshipPropertiesRepository + extends Neo4jRepository { + + } + + interface SameIdEntitiesRepository extends Neo4jRepository { + + } + + interface EntityWithCustomIdAndDynamicLabelsRepository + extends Neo4jRepository { + + } + + interface TemporalRepository extends Neo4jRepository { + + List findAllByProperty1After(OffsetDateTime aValue); + + List findAllByProperty2After(LocalTime aValue); + + } + + @SpringJUnitConfig(Config.class) + abstract static class IntegrationTestBase { + + @Autowired + private Driver driver; + + @Autowired + private TransactionTemplate transactionalOperator; + + @Autowired + private BookmarkCapture bookmarkCapture; + + void setupData(TransactionContext transaction) { + } + + @BeforeEach + void before() { + doWithSession(session -> session.executeWrite(tx -> { + tx.run("MATCH (n) detach delete n").consume(); + setupData(tx); + return null; + })); + } + + T doWithSession(Function sessionConsumer) { + try (Session session = this.driver.session(this.bookmarkCapture + .createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { + T result = sessionConsumer.apply(session); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + return result; + } + } + + void assertWithSession(Consumer consumer) { + + try (Session session = this.driver.session(this.bookmarkCapture + .createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { + consumer.accept(session); + } + } + + } + + @Configuration + @EnableNeo4jRepositories(considerNestedRepositories = true) + @EnableTransactionManagement + static class Config extends Neo4jImperativeTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Override + protected Collection getMappingBasePackages() { + return Arrays.asList(PersonWithAllConstructor.class.getPackage().getName(), + Flight.class.getPackage().getName()); + } + + @Bean + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + mappingContext.setStrict(true); + + return mappingContext; + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseSelectionProvider) { + + return Neo4jTransactionManager.with(driver) + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .withBookmarkManager(Neo4jBookmarkManager.create(bookmarkCapture())) + .build(); + } + + @Override + public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { + + return Neo4jClient.with(driver) + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .build(); + } + + @Bean + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + return new TransactionTemplate(transactionManager); + } + + @Override + public DatabaseSelectionProvider databaseSelectionProvider() { + return () -> databaseSelection.get(); + } + + @Bean + UserSelectionProvider getUserSelectionProvider() { + return () -> userSelection.get(); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } @Nested @TestPropertySource(properties = "foo=Test") class Find extends IntegrationTestBase { + static Stream basicScrollSupportFor(@Autowired PersonRepository repository) { + return Stream.of(Arguments.of(repository, ScrollPosition.keyset()), + Arguments.of(repository, ScrollPosition.offset())); + } + @Override void setupData(TransactionContext transaction) { ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized()); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ, "createdAt", createdAt) - ).next().get(0).asLong(); - id2 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", - Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO) - ).next().get(0).asLong(); + RepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ, "createdAt", createdAt)) + .next() + .get(0) + .asLong(); + RepositoryIT.this.id2 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", + Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (n:KotlinPerson), " - + " (n)-[:WORKS_IN{since: 2019}]->(:KotlinClub{name: 'Golf club'}) SET n.name = '" + TEST_PERSON1_NAME + "'"); - transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); + + " (n)-[:WORKS_IN{since: 2019}]->(:KotlinClub{name: 'Golf club'}) SET n.name = '" + + TEST_PERSON1_NAME + "'"); + transaction + .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); IntStream.rangeClosed(1, 20) - .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", - Values.parameters("i", String.format("%02d", i)))); + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + RepositoryIT.this.person1 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, + TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", + Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); + RepositoryIT.this.person2 = new PersonWithAllConstructor(RepositoryIT.this.id2, TEST_PERSON2_NAME, + TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, + Collections.emptyList(), SFO, null); transaction.run(""" CREATE (lhr:Airport {code: 'LHR', name: 'London Heathrow'}) @@ -285,25 +783,19 @@ class RepositoryIT { assertThat(people).extracting("name").containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); } - static Stream basicScrollSupportFor(@Autowired PersonRepository repository) { - return Stream.of(Arguments.of(repository, ScrollPosition.keyset()), Arguments.of(repository, ScrollPosition.offset())); - } - @ParameterizedTest(name = "basicScrollSupportFor {1}") @MethodSource void basicScrollSupportFor(PersonRepository repository, ScrollPosition initialPosition) { - var it = WindowIterator.of(repository::findTop1ByOrderByName) - .startingAt(initialPosition); + var it = WindowIterator.of(repository::findTop1ByOrderByName).startingAt(initialPosition); var content = new ArrayList(); while (it.hasNext()) { var next = it.next(); content.add(next); } - assertThat(content) - .hasSize(2) - .extracting(PersonWithAllConstructor::getName) - .containsExactly("Test", "Test2"); + assertThat(content).hasSize(2) + .extracting(PersonWithAllConstructor::getName) + .containsExactly("Test", "Test2"); } @Test @@ -317,7 +809,7 @@ class RepositoryIT { @Test void findById(@Autowired PersonRepository repository) { - Optional person = repository.findById(id1); + Optional person = repository.findById(RepositoryIT.this.id1); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @@ -346,7 +838,8 @@ class RepositoryIT { @Test void findAllById(@Autowired PersonRepository repository) { - List persons = repository.findAllById(Arrays.asList(id1, id2)); + List persons = repository + .findAllById(Arrays.asList(RepositoryIT.this.id1, RepositoryIT.this.id2)); assertThat(persons).hasSize(2); } @@ -360,7 +853,8 @@ class RepositoryIT { AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L); anotherThing.setName("Bart"); - assertThat(optionalThing).map(ThingWithAssignedId::getThings).contains(Collections.singletonList(anotherThing)); + assertThat(optionalThing).map(ThingWithAssignedId::getThings) + .contains(Collections.singletonList(anotherThing)); } @Test @@ -376,7 +870,8 @@ class RepositoryIT { void findAllByConvertedId(@Autowired EntityWithConvertedIdRepository repository) { doWithSession(session -> session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})").consume()); - List entities = repository.findAllById(Collections.singleton(EntityWithConvertedId.IdentifyingEnum.A)); + List entities = repository + .findAllById(Collections.singleton(EntityWithConvertedId.IdentifyingEnum.A)); assertThat(entities).hasSize(1); assertThat(entities.get(0).getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A); @@ -399,7 +894,7 @@ class RepositoryIT { List persons = repository.findAll(Sort.by("name")); - assertThat(persons).containsExactly(person1, person2); + assertThat(persons).containsExactly(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test @@ -407,7 +902,7 @@ class RepositoryIT { List persons = repository.findAll(Sort.by(Sort.Order.asc("name"))); - assertThat(persons).containsExactly(person1, person2); + assertThat(persons).containsExactly(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test @@ -415,40 +910,36 @@ class RepositoryIT { List persons = repository.findAll(Sort.by(Sort.Order.desc("name"))); - assertThat(persons).containsExactly(person2, person1); + assertThat(persons).containsExactly(RepositoryIT.this.person2, RepositoryIT.this.person1); } @Test // GH-2274 void findAllWithSortWithCaseIgnored(@Autowired PersonRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> { - tx.run("CREATE (n:PersonWithAllConstructor {name: 'Ab', firstName: 'n/a'})"); - tx.run("CREATE (n:PersonWithAllConstructor {name: 'aa', firstName: 'n/a'})"); - return null; - })); + doWithSession(session -> session.executeWrite(tx -> { + tx.run("CREATE (n:PersonWithAllConstructor {name: 'Ab', firstName: 'n/a'})"); + tx.run("CREATE (n:PersonWithAllConstructor {name: 'aa', firstName: 'n/a'})"); + return null; + })); List persons = repository.findAll(Sort.by(Sort.Order.asc("name").ignoreCase())); - assertThat(persons) - .extracting(PersonWithAllConstructor::getName) - .containsExactly("aa", "Ab", "Test", "Test2"); + assertThat(persons).extracting(PersonWithAllConstructor::getName) + .containsExactly("aa", "Ab", "Test", "Test2"); } @Test // GH-2274 void findAllWithSortWithCaseIgnoredSpelBased(@Autowired PersonRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> { - tx.run("CREATE (n:PersonWithAllConstructor {name: 'Ab', firstName: 'n/a'})"); - tx.run("CREATE (n:PersonWithAllConstructor {name: 'aa', firstName: 'n/a'})"); - return null; - })); + doWithSession(session -> session.executeWrite(tx -> { + tx.run("CREATE (n:PersonWithAllConstructor {name: 'Ab', firstName: 'n/a'})"); + tx.run("CREATE (n:PersonWithAllConstructor {name: 'aa', firstName: 'n/a'})"); + return null; + })); List persons = repository - .orderBySpel(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("n.name").ignoreCase()))); - assertThat(persons) - .extracting(PersonWithAllConstructor::getName) - .containsExactly("aa", "Ab", "Test", "Test2"); + .orderBySpel(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("n.name").ignoreCase()))); + assertThat(persons).extracting(PersonWithAllConstructor::getName) + .containsExactly("aa", "Ab", "Test", "Test2"); } @Test @@ -459,11 +950,11 @@ class RepositoryIT { int limit = 1; Page persons = repository.findAll(PageRequest.of(page, limit, sort)); - assertThat(persons).containsExactly(person1); + assertThat(persons).containsExactly(RepositoryIT.this.person1); page = 1; persons = repository.findAll(PageRequest.of(page, limit, sort)); - assertThat(persons).containsExactly(person2); + assertThat(persons).containsExactly(RepositoryIT.this.person2); } @Test @@ -474,49 +965,48 @@ class RepositoryIT { assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); } - @Test // DATAGRAPH-1429 + @Test // DATAGRAPH-1429 void aggregateThroughQueryIntoListShouldWork(@Autowired PersonRepository repository) { List people = repository.aggregateAllPeople(); - assertThat(people) - .hasSize(2) - .extracting(PersonWithAllConstructor::getName) - .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + assertThat(people).hasSize(2) + .extracting(PersonWithAllConstructor::getName) + .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); } - @Test // DATAGRAPH-1429 + @Test // DATAGRAPH-1429 void aggregateThroughQueryIntoCustomObjectShouldWork(@Autowired PersonRepository repository) { PersonRepository.CustomAggregation customAggregation = repository.aggregateAllPeopleCustom(); - assertThat(customAggregation) - .hasSize(2) - .extracting(PersonWithAllConstructor::getName) - .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + assertThat(customAggregation).hasSize(2) + .extracting(PersonWithAllConstructor::getName) + .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); } @Test // DATAGRAPH-1429 void aggregateThroughQueryIntoCustomObjectDTOShouldWork(@Autowired PersonRepository repository) { PersonRepository.CustomAggregationOfDto customAggregation = repository - .findAllDtoProjectionsWithAdditionalPropertiesAsCustomAggregation(TEST_PERSON1_NAME); - assertThat(customAggregation) - .isNotEmpty(); - assertThat(customAggregation.getBySomeLongValue(4711L)) - .satisfies(dto -> { - assertThat(dto.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); - assertThat(dto.getSomeDoubles()).containsExactly(21.42, 42.21); - assertThat(dto.getOtherPeople()).hasSize(1) - .first() - .extracting(PersonWithAllConstructor::getFirstName) - .isEqualTo(TEST_PERSON2_FIRST_NAME); - }); + .findAllDtoProjectionsWithAdditionalPropertiesAsCustomAggregation(TEST_PERSON1_NAME); + assertThat(customAggregation).isNotEmpty(); + assertThat(customAggregation.getBySomeLongValue(4711L)).satisfies(dto -> { + assertThat(dto.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); + assertThat(dto.getSomeDoubles()).containsExactly(21.42, 42.21); + assertThat(dto.getOtherPeople()).hasSize(1) + .first() + .extracting(PersonWithAllConstructor::getFirstName) + .isEqualTo(TEST_PERSON2_FIRST_NAME); + }); } - @Test // DATAGRAPH-1429 - void queryAggregatesShouldWorkWithTheTemplate(@Autowired Neo4jTemplate template, @Autowired PlatformTransactionManager transactionManager) { + @Test // DATAGRAPH-1429 + void queryAggregatesShouldWorkWithTheTemplate(@Autowired Neo4jTemplate template, + @Autowired PlatformTransactionManager transactionManager) { new TransactionTemplate(transactionManager).executeWithoutResult(tx -> { - List people = template.findAll("unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", Person.class); + List people = template.findAll( + "unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", + Person.class); assertThat(people).extracting(Person::getFirstName).containsExactly("1", "2", "3", "4", "5"); }); } @@ -555,7 +1045,7 @@ class RepositoryIT { void loadOptionalPersonWithAllConstructorWithSpelParameters(@Autowired PersonRepository repository) { Optional person = repository - .getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + .getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @@ -563,26 +1053,27 @@ class RepositoryIT { @Test void loadOptionalPersonWithAllConstructorWithPropertyPlacholder(@Autowired PersonRepository repository) { - Optional person = repository - .getOptionalPersonViaPropertyPlaceholder(); + Optional person = repository.getOptionalPersonViaPropertyPlaceholder(); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @Test - void loadOptionalPersonWithAllConstructorWithSpelParametersAndDynamicSort(@Autowired PersonRepository repository) { + void loadOptionalPersonWithAllConstructorWithSpelParametersAndDynamicSort( + @Autowired PersonRepository repository) { - Optional person = repository - .getOptionalPersonViaQueryWithSort(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2), Sort.by("n.name").ascending()); + Optional person = repository.getOptionalPersonViaQueryWithSort( + TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2), Sort.by("n.name").ascending()); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @Test - void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery(@Autowired PersonRepository repository) { + void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery( + @Autowired PersonRepository repository) { Optional person = repository - .getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + .getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @@ -593,7 +1084,7 @@ class RepositoryIT { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); assertThat(persons).extracting(PersonWithNoConstructor::getName, PersonWithNoConstructor::getFirstName) - .containsExactlyInAnyOrder(Tuple.tuple(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)); + .containsExactlyInAnyOrder(Tuple.tuple(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)); } @Test @@ -671,13 +1162,13 @@ class RepositoryIT { List persons; persons = repository.findAllBySameValue(TEST_PERSON_SAMEVALUE); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); persons = repository.findAllBySameValueIgnoreCase(TEST_PERSON_SAMEVALUE.toUpperCase()); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); persons = repository.findAllByBornOn(TEST_PERSON1_BORN_ON); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test @@ -691,7 +1182,7 @@ class RepositoryIT { void findByPropertyThatNeedsConversion(@Autowired PersonRepository repository) { List people = repository - .findAllByPlace(new GeographicPoint2d(NEO4J_HQ.y(), NEO4J_HQ.x())); + .findAllByPlace(new GeographicPoint2d(NEO4J_HQ.y(), NEO4J_HQ.x())); assertThat(people).hasSize(1); } @@ -699,8 +1190,8 @@ class RepositoryIT { @Test void findByPropertyFailsIfNoConverterIsAvailable(@Autowired PersonRepository repository) { assertThatExceptionOfType(ConverterNotFoundException.class) - .isThrownBy(() -> repository.findAllByPlace(new PersonRepository.SomethingThatIsNotKnownAsEntity())) - .withMessageStartingWith("No converter found capable of converting from type"); + .isThrownBy(() -> repository.findAllByPlace(new PersonRepository.SomethingThatIsNotKnownAsEntity())) + .withMessageStartingWith("No converter found capable of converting from type"); } @Test @@ -709,11 +1200,11 @@ class RepositoryIT { Optional optionalPerson; optionalPerson = repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); - assertThat(optionalPerson).isPresent().contains(person1); + assertThat(optionalPerson).isPresent().contains(RepositoryIT.this.person1); optionalPerson = repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON1_FIRST_NAME.toUpperCase()); - assertThat(optionalPerson).isPresent().contains(person1); + assertThat(optionalPerson).isPresent().contains(RepositoryIT.this.person1); } @Test // GH-112 @@ -738,14 +1229,16 @@ class RepositoryIT { @Test void findBySimplePropertiesOred(@Autowired PersonRepository repository) { - List persons = repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + List persons = repository.findAllByNameOrName(TEST_PERSON1_NAME, + TEST_PERSON2_NAME); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test // DATAGRAPH-1374 void findSliceShouldWork(@Autowired PersonRepository repository) { - Slice slice = repository.findSliceByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("name").descending())); + Slice slice = repository.findSliceByNameOrName(TEST_PERSON1_NAME, + TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("name").descending())); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME); assertThat(slice.hasNext()).isTrue(); @@ -760,7 +1253,8 @@ class RepositoryIT { void customFindMapsDeepRelationships(@Autowired PetRepository repository) { Record record = doWithSession(session -> session.run( - "CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3").single()); + "CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3") + .single()); long petNode1Id = TestIdentitySupport.getInternalId(record.get("p1").asNode()); long petNode2Id = TestIdentitySupport.getInternalId(record.get("p2").asNode()); @@ -778,9 +1272,9 @@ class RepositoryIT { @Test // GH-2345 void customFindHydratesIncompleteCustomQueryObjectsCorrect(@Autowired PetRepository repository) { - doWithSession(session -> - session.run("CREATE (:Pet{name: 'Luna'})-[:Has]->(:Pet{name:'Luna'})-[:Has]->(:Pet{name:'Daphne'})").consume() - ); + doWithSession(session -> session + .run("CREATE (:Pet{name: 'Luna'})-[:Has]->(:Pet{name:'Luna'})-[:Has]->(:Pet{name:'Daphne'})") + .consume()); List pets = repository.findLunas(); assertThat(pets).hasSize(2); @@ -790,9 +1284,9 @@ class RepositoryIT { @Test // GH-2345 void customFindFailsOnHydrationOfCustomQueryObjectsIfImmutable(@Autowired ImmutablePetRepository repository) { - doWithSession(session -> - session.run("CREATE (:ImmutablePet{name: 'Luna'})-[:Has]->(:ImmutablePet{name:'Luna'})-[:Has]->(:ImmutablePet{name:'Daphne'})").consume() - ); + doWithSession(session -> session.run( + "CREATE (:ImmutablePet{name: 'Luna'})-[:Has]->(:ImmutablePet{name:'Luna'})-[:Has]->(:ImmutablePet{name:'Daphne'})") + .consume()); assertThatExceptionOfType(MappingException.class).isThrownBy(repository::findLunas); } @@ -844,12 +1338,14 @@ class RepositoryIT { @Test // DATAGRAPH-1440 void findSliceByCustomQueryWithoutCount(@Autowired PersonRepository repository) { - Slice slice = repository.findSliceByCustomQueryWithoutCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.unsorted())); + Slice slice = repository.findSliceByCustomQueryWithoutCount(TEST_PERSON1_NAME, + TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.unsorted())); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME); assertThat(slice.hasNext()).isTrue(); - slice = repository.findSliceByCustomQueryWithoutCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, slice.nextPageable()); + slice = repository.findSliceByCustomQueryWithoutCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, + slice.nextPageable()); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME); assertThat(slice.hasNext()).isFalse(); @@ -858,12 +1354,14 @@ class RepositoryIT { @Test // DATAGRAPH-1440 void findSliceByCustomQueryWithCountShouldWork(@Autowired PersonRepository repository) { - Slice slice = repository.findSliceByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.unsorted())); + Slice slice = repository.findSliceByCustomQueryWithCount(TEST_PERSON1_NAME, + TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.unsorted())); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME); assertThat(slice.hasNext()).isTrue(); - slice = repository.findSliceByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, slice.nextPageable()); + slice = repository.findSliceByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, + slice.nextPageable()); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME); assertThat(slice.hasNext()).isFalse(); @@ -873,21 +1371,22 @@ class RepositoryIT { void filtersOnSameEntitiesButDifferentRelationsShouldWork(@Autowired FlightRepository repository) { List flights = repository.findAllByDepartureCodeAndArrivalCode("LHR", "LAX"); - assertThat(flights).hasSize(1) - .first().extracting(Flight::getName).isEqualTo("FL 001"); + assertThat(flights).hasSize(1).first().extracting(Flight::getName).isEqualTo("FL 001"); } @Test // GH-2239 void findPageByCustomQueryWithCountShouldWork(@Autowired PersonRepository repository) { - Page slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("n.name").descending())); + Page slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, + TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("n.name").descending())); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME); assertThat(slice.hasNext()).isTrue(); assertThat(slice.getTotalElements()).isEqualTo(2); assertThat(slice.getTotalPages()).isEqualTo(2); - slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, slice.nextPageable()); + slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, + slice.nextPageable()); assertThat(slice.getSize()).isEqualTo(1); assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME); assertThat(slice.hasNext()).isFalse(); @@ -897,10 +1396,9 @@ class RepositoryIT { @Test void findEntityPointingToEqualEntity(@Autowired PetRepository repository) { - doWithSession(session -> - session - .run("CREATE (:Pet{name: 'Pet2'})-[:Has]->(p1:Pet{name: 'Pet1'})-[:Has]->(p1) RETURN p1") - .consume()); + doWithSession(session -> session + .run("CREATE (:Pet{name: 'Pet2'})-[:Has]->(p1:Pet{name: 'Pet1'})-[:Has]->(p1) RETURN p1") + .consume()); List allPets = repository.findAllFriends(); for (Pet pet : allPets) { @@ -912,6 +1410,7 @@ class RepositoryIT { } } } + } @Nested @@ -920,14 +1419,12 @@ class RepositoryIT { @Test void findEntityWithRelationship(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), - (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), - (p1)-[:Has]->(p2)RETURN n, h1, h2, p1, p2, c - """) - .single()); + Record record = doWithSession(session -> session.run(""" + CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), + (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), + (p1)-[:Has]->(p2)RETURN n, h1, h2, p1, p2, c + """).single()); Node personNode = record.get("n").asNode(); Node clubNode = record.get("c").asNode(); @@ -974,7 +1471,8 @@ class RepositoryIT { void findDeepSameLabelsAndTypeRelationships(@Autowired PetRepository repository) { Record record = doWithSession(session -> session.run( - "CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3").single()); + "CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3") + .single()); long petNode1Id = TestIdentitySupport.getInternalId(record.get("p1").asNode()); long petNode2Id = TestIdentitySupport.getInternalId(record.get("p2").asNode()); @@ -992,7 +1490,8 @@ class RepositoryIT { @Test void findBySameLabelRelationshipProperty(@Autowired PetRepository repository) { - doWithSession(session -> session.run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})").consume()); + doWithSession( + session -> session.run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})").consume()); Pet pet = repository.findByFriendsName("Pet2"); assertThat(pet).isNotNull(); @@ -1002,7 +1501,8 @@ class RepositoryIT { @Test void deleteByOwnPropertyAndRelationshipsProperty(@Autowired PetRepository repository) { - doWithSession(session -> session.run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})").consume()); + doWithSession( + session -> session.run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})").consume()); repository.deleteByNameAndFriendsName("Pet1", "Pet2"); doWithSession(session -> { @@ -1013,7 +1513,9 @@ class RepositoryIT { @Test void findBySameLabelRelationshipPropertyMultipleLevels(@Autowired PetRepository repository) { - doWithSession(session -> session.run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})-[:Has]->(p3:Pet{name: 'Pet3'})").consume()); + doWithSession(session -> session + .run("CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'})-[:Has]->(p3:Pet{name: 'Pet3'})") + .consume()); Pet pet = repository.findByFriendsFriendsName("Pet3"); assertThat(pet).isNotNull(); @@ -1025,19 +1527,18 @@ class RepositoryIT { void findLoopingDeepRelationships(@Autowired LoopingRelationshipRepository loopingRelationshipRepository) { long type1Id = TestIdentitySupport.getInternalId(doWithSession(session -> session.run(""" - CREATE (t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)RETURN t1 - """ - ).single().get("t1").asNode())); + CREATE (t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)RETURN t1 + """).single().get("t1").asNode())); DeepRelationships.LoopingType1 type1 = loopingRelationshipRepository.findById(type1Id).get(); @@ -1067,9 +1568,9 @@ class RepositoryIT { @Test void findEntityWithRelationshipToTheSameNode(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (p1)-[:Has]->(h1)RETURN n, h1, p1") - .single()); + Record record = doWithSession(session -> session.run( + "CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (p1)-[:Has]->(h1)RETURN n, h1, p1") + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1099,21 +1600,24 @@ class RepositoryIT { } @Test - void findEntityWithBidirectionalRelationshipInConstructorThrowsException(@Autowired BidirectionalStartRepository repository) { + void findEntityWithBidirectionalRelationshipInConstructorThrowsException( + @Autowired BidirectionalStartRepository repository) { - var node = doWithSession(session -> session - .run(""" - CREATE - (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}), - (e)<-[:ANOTHER_CONNECTION]-(anotherStart:BidirectionalStart{name:'Elmo'}) - RETURN n""" - ) - .single().get("n").asNode()); + var node = doWithSession(session -> session + .run(""" + CREATE + (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}), + (e)<-[:ANOTHER_CONNECTION]-(anotherStart:BidirectionalStart{name:'Elmo'}) + RETURN n""") + .single() + .get("n") + .asNode()); assertThatThrownBy(() -> repository.findById(TestIdentitySupport.getInternalId(node))) - .hasRootCauseMessage("The node with id " + node.elementId() + " has a logical cyclic mapping dependency; " + - "its creation caused the creation of another node that has a reference to this") - .hasRootCauseInstanceOf(MappingException.class); + .hasRootCauseMessage( + "The node with id " + node.elementId() + " has a logical cyclic mapping dependency; " + + "its creation caused the creation of another node that has a reference to this") + .hasRootCauseInstanceOf(MappingException.class); } @@ -1153,15 +1657,20 @@ class RepositoryIT { private long createFriendlyPets() { return doWithSession(session -> session.run( - "CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(:Pet{name:'Tom'})RETURN id(luna) as id").single().get("id").asLong()); + "CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(:Pet{name:'Tom'})RETURN id(luna) as id") + .single() + .get("id") + .asLong()); } @Test void findEntityWithBidirectionalRelationshipFromIncomingSide(@Autowired BidirectionalEndRepository repository) { long endId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run( - "CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e") - .single().get("e").asNode())); + "CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e") + .single() + .get("e") + .asNode())); Optional entityOptional = repository.findById(endId); assertThat(entityOptional).isPresent(); @@ -1173,15 +1682,16 @@ class RepositoryIT { @Test void findMultipleEntitiesWithRelationship(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p") - .single()); + Record record = doWithSession(session -> session.run( + "CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p") + .single()); long hobbyNode1Id = TestIdentitySupport.getInternalId(record.get("h").asNode()); long petNode1Id = TestIdentitySupport.getInternalId(record.get("p").asNode()); record = doWithSession(session -> session.run( - "CREATE (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), (n)-[:Has]->(p:Pet{name: 'Jerry2'}) RETURN n, h, p").single()); + "CREATE (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), (n)-[:Has]->(p:Pet{name: 'Jerry2'}) RETURN n, h, p") + .single()); long hobbyNode2Id = TestIdentitySupport.getInternalId(record.get("h").asNode()); long petNode2Id = TestIdentitySupport.getInternalId(record.get("p").asNode()); @@ -1207,9 +1717,9 @@ class RepositoryIT { @Test void findEntityWithRelationshipViaQuery(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) RETURN n, h1, p1, p2") - .single()); + Record record = doWithSession(session -> session.run( + "CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) RETURN n, h1, p1, p2") + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1239,15 +1749,13 @@ class RepositoryIT { @Test void findEntityWithRelationshipViaPathQuery(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """ - ).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1278,8 +1786,10 @@ class RepositoryIT { void findEntityWithRelationshipWithAssignedId(@Autowired PetRepository repository) { long petNodeId = TestIdentitySupport.getInternalId(doWithSession(session -> session - .run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t").single() - .get("p").asNode())); + .run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t") + .single() + .get("p") + .asNode())); Pet pet = repository.findById(petNodeId).get(); ThingWithAssignedId relatedThing = pet.getThings().get(0); @@ -1288,11 +1798,16 @@ class RepositoryIT { } @Test // DATAGRAPH-1431 - void findAndMapMultipleLevelsOfSimpleRelationships(@Autowired SimpleEntityWithRelationshipARepository repository) { - Long aId = doWithSession(session -> session.executeWrite(tx -> tx.run(""" - CREATE (a:SimpleEntityWithRelationshipA)-[:TO_B]->(:SimpleEntityWithRelationshipB)-[:TO_C]->(:SimpleEntityWithRelationshipC) - RETURN id(a) as aId - """).single().get("aId").asLong())); + void findAndMapMultipleLevelsOfSimpleRelationships( + @Autowired SimpleEntityWithRelationshipARepository repository) { + Long aId = doWithSession(session -> session.executeWrite(tx -> tx + .run(""" + CREATE (a:SimpleEntityWithRelationshipA)-[:TO_B]->(:SimpleEntityWithRelationshipB)-[:TO_C]->(:SimpleEntityWithRelationshipC) + RETURN id(a) as aId + """) + .single() + .get("aId") + .asLong())); SimpleEntityWithRelationshipA entityA = repository.findById(aId).get(); assertThat(entityA).isNotNull(); @@ -1302,14 +1817,12 @@ class RepositoryIT { @Test // GH-2175 void findCyclicWithPageable(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run(""" + doWithSession(session -> session.run(""" CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), (p1)-[:Has]->(p2) - """).consume() - ); + """).consume()); Page peoplePage = repository.findAll(PageRequest.of(0, 1)); assertThat(peoplePage.getTotalElements()).isEqualTo(1); @@ -1317,15 +1830,13 @@ class RepositoryIT { @Test // GH-2175 void findCyclicWithSort(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run(""" + doWithSession(session -> session.run(""" CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), (p1)-[:Has]->(p2) - """).consume() - ); + """).consume()); List people = repository.findAll(Sort.by("name")); assertThat(people).hasSize(1); @@ -1333,15 +1844,13 @@ class RepositoryIT { @Test // GH-2175 void cyclicDerivedFinderWithPageable(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run(""" + doWithSession(session -> session.run(""" CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), (p1)-[:Has]->(p2) - """).consume() - ); + """).consume()); Page peoplePage = repository.findByName("Freddie", PageRequest.of(0, 1)); assertThat(peoplePage.getTotalElements()).isEqualTo(1); @@ -1349,15 +1858,13 @@ class RepositoryIT { @Test // GH-2175 void cyclicDerivedFinderWithSort(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run(""" + doWithSession(session -> session.run(""" CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), (p1)-[:Has]->(p2) - """).consume() - ); + """).consume()); List people = repository.findByName("Freddie", Sort.by("name")); assertThat(people).hasSize(1); @@ -1365,14 +1872,13 @@ class RepositoryIT { private void createOneToOneScenario() { doWithSession(session -> { - try (Transaction tx = session.beginTransaction()) { - tx.run("CREATE (s:OneToOneSource {name: 's1'}) -[:OWNS]->(t:OneToOneTarget {name: 't1'})"); - tx.run("CREATE (s:OneToOneSource {name: 's2'}) -[:OWNS]->(t:OneToOneTarget {name: 't2'})"); - tx.commit(); - } - return null; - } - ); + try (Transaction tx = session.beginTransaction()) { + tx.run("CREATE (s:OneToOneSource {name: 's1'}) -[:OWNS]->(t:OneToOneTarget {name: 't1'})"); + tx.run("CREATE (s:OneToOneSource {name: 's2'}) -[:OWNS]->(t:OneToOneTarget {name: 't2'})"); + tx.commit(); + } + return null; + }); } private void assertOneToOneScenario(List oneToOnes) { @@ -1382,7 +1888,7 @@ class RepositoryIT { } @Test // GH-2269 - void shouldFindOneToOneWithDefault(@Autowired OneToOneRepository repository) { + void shouldFindOneToOneWithDefault(@Autowired OneToOneRepository repository) { createOneToOneScenario(); List oneToOnes = repository.findAll(); @@ -1390,7 +1896,7 @@ class RepositoryIT { } @Test // GH-2269 - void shouldFindOneToOneWithCollect(@Autowired OneToOneRepository repository) { + void shouldFindOneToOneWithCollect(@Autowired OneToOneRepository repository) { createOneToOneScenario(); List oneToOnes = repository.findAllWithCustomQuery(); @@ -1398,7 +1904,7 @@ class RepositoryIT { } @Test // GH-2269 - void shouldFindOneToOneWithoutCollect(@Autowired OneToOneRepository repository) { + void shouldFindOneToOneWithoutCollect(@Autowired OneToOneRepository repository) { createOneToOneScenario(); List oneToOnes = repository.findAllWithCustomQueryNoCollect(); @@ -1406,15 +1912,17 @@ class RepositoryIT { } @Test // GH-2269 - void shouldFindOne(@Autowired OneToOneRepository repository) { + void shouldFindOne(@Autowired OneToOneRepository repository) { createOneToOneScenario(); Optional optionalSource = repository.findOneByName("s1"); - assertThat(optionalSource).hasValueSatisfying(s -> assertThat(s).extracting(OneToOneSource::getTarget).extracting(OneToOneTarget::getName).isEqualTo("t1")); + assertThat(optionalSource).hasValueSatisfying(s -> assertThat(s).extracting(OneToOneSource::getTarget) + .extracting(OneToOneTarget::getName) + .isEqualTo("t1")); } @Test // GH-2269 - void shouldFindOneToOneWithWildcardReturn(@Autowired OneToOneRepository repository) { + void shouldFindOneToOneWithWildcardReturn(@Autowired OneToOneRepository repository) { createOneToOneScenario(); List oneToOnes = repository.findAllWithCustomQueryReturnStar(); @@ -1423,52 +1931,57 @@ class RepositoryIT { private void createOneToOneScenarioForNullValues() { doWithSession(session -> { - try (Transaction tx = session.beginTransaction()) { - tx.run("CREATE (s:OneToOneSource {name: 's1'}) -[:OWNS]->(t:OneToOneTarget {name: 't1'})"); - tx.run("CREATE (s:OneToOneSource {name: 's2'})"); - tx.commit(); - } - return null; - } - ); + try (Transaction tx = session.beginTransaction()) { + tx.run("CREATE (s:OneToOneSource {name: 's1'}) -[:OWNS]->(t:OneToOneTarget {name: 't1'})"); + tx.run("CREATE (s:OneToOneSource {name: 's2'})"); + tx.commit(); + } + return null; + }); } private void assertOneToOneScenarioWithNulls(List oneToOnes) { assertThat(oneToOnes).hasSize(2); - assertThat(oneToOnes).extracting(OneToOneSource.OneToOneSourceProjection::getName).containsExactlyInAnyOrder("s1", "s2"); + assertThat(oneToOnes).extracting(OneToOneSource.OneToOneSourceProjection::getName) + .containsExactlyInAnyOrder("s1", "s2"); assertThat(oneToOnes).filteredOn(s -> s.getTarget() != null) - .extracting(s -> s.getTarget().getName()).containsExactly("t1"); + .extracting(s -> s.getTarget().getName()) + .containsExactly("t1"); } @Test // GH-2305 - void shouldFindOneToOneWithNullValues(@Autowired OneToOneRepository repository) { + void shouldFindOneToOneWithNullValues(@Autowired OneToOneRepository repository) { createOneToOneScenarioForNullValues(); List oneToOnes = repository.findAllWithNullValues(); assertOneToOneScenarioWithNulls(oneToOnes); } + } @Nested class RelationshipProperties extends IntegrationTestBase { @Test // DATAGRAPH-1397 - void shouldBeStorableOnSets( - @Autowired Neo4jTemplate template) { + void shouldBeStorableOnSets(@Autowired Neo4jTemplate template) { - var hlp = doWithSession(session -> session.run("CREATE (n:PersonWithRelationshipWithProperties2{name:'Freddie'})," - + " (n)-[l1:LIKES " - + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" - + "]->(h1:Hobby{name:'Music'}), " - + "(n)-[l2:LIKES " - + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" - + "]->(h2:Hobby{name:'Something else'})" - + "RETURN n, h1, h2").single().get("n").asNode()); + var hlp = doWithSession(session -> session + .run("CREATE (n:PersonWithRelationshipWithProperties2{name:'Freddie'})," + " (n)-[l1:LIKES " + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'}), " + "(n)-[l2:LIKES " + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'})" + "RETURN n, h1, h2") + .single() + .get("n") + .asNode()); long personId = TestIdentitySupport.getInternalId(hlp); - Optional optionalPerson = template.findById(personId, PersonWithRelationshipWithProperties2.class); + Optional optionalPerson = template.findById(personId, + PersonWithRelationshipWithProperties2.class); assertThat(optionalPerson).hasValueSatisfying(person -> { assertThat(person.getName()).isEqualTo("Freddie"); - assertThat(person.getHobbies()).hasSize(2).extracting(LikesHobbyRelationship::getSince).containsExactlyInAnyOrder(1995, 2000); + assertThat(person.getHobbies()).hasSize(2) + .extracting(LikesHobbyRelationship::getSince) + .containsExactlyInAnyOrder(1995, 2000); }); } @@ -1476,17 +1989,15 @@ class RepositoryIT { void findEntityWithRelationshipWithProperties( @Autowired PersonWithRelationshipWithPropertiesRepository repository) { - Record record = doWithSession(session -> session.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," - + " (n)-[l1:LIKES " - + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" - + "]->(h1:Hobby{name:'Music'}), " - + "(n)-[l2:LIKES " - + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" - + "]->(h2:Hobby{name:'Something else'}), " - + "(n) - [:OWNS] -> (p:Pet {name: 'A Pet'}), " - + "(n) - [:OWNS {place: 'The place to be'}] -> (c1:Club {name: 'Berlin Mitte'}), " - + "(n) - [:OWNS {place: 'Whatever'}] -> (c2:Club {name: 'Schachklub'}) " - + "RETURN n, h1, h2").single()); + Record record = doWithSession(session -> session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + " (n)-[l1:LIKES " + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'}), " + "(n)-[l2:LIKES " + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'}), " + "(n) - [:OWNS] -> (p:Pet {name: 'A Pet'}), " + + "(n) - [:OWNS {place: 'The place to be'}] -> (c1:Club {name: 'Berlin Mitte'}), " + + "(n) - [:OWNS {place: 'Whatever'}] -> (c2:Club {name: 'Schachklub'}) " + "RETURN n, h1, h2") + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1526,18 +2037,22 @@ class RepositoryIT { assertThat(hobbies.get(hobbies.indexOf(rel2)).getHobby()).isEqualTo(hobby2); assertThat(person.getClubs()).hasSize(2) - .extracting(ClubRelationship::getPlace) - .containsExactlyInAnyOrder("The place to be", "Whatever"); + .extracting(ClubRelationship::getPlace) + .containsExactlyInAnyOrder("The place to be", "Whatever"); } @Test - void findEntityWithRelationshipWithPropertiesScalar(@Autowired PersonWithRelationshipWithPropertiesRepository repository) { + void findEntityWithRelationshipWithPropertiesScalar( + @Autowired PersonWithRelationshipWithPropertiesRepository repository) { - long personId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," - + " (n)-[:WORKS_IN{since: 1995}]->(:Club{name:'Blubb'})," - + "(n) - [:OWNS {place: 'The place to be'}] -> (c1:Club {name: 'Berlin Mitte'}), " - + "(n) - [:OWNS {place: 'Whatever'}] -> (c2:Club {name: 'Schachklub'}) " - + "RETURN n").single().get("n").asNode())); + long personId = TestIdentitySupport.getInternalId(doWithSession(session -> session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + + " (n)-[:WORKS_IN{since: 1995}]->(:Club{name:'Blubb'})," + + "(n) - [:OWNS {place: 'The place to be'}] -> (c1:Club {name: 'Berlin Mitte'}), " + + "(n) - [:OWNS {place: 'Whatever'}] -> (c2:Club {name: 'Schachklub'}) " + "RETURN n") + .single() + .get("n") + .asNode())); PersonWithRelationshipWithProperties person = repository.findById(personId).get(); @@ -1547,12 +2062,14 @@ class RepositoryIT { } @Test - void findEntityWithRelationshipWithPropertiesSameLabel( - @Autowired FriendRepository repository) { + void findEntityWithRelationshipWithPropertiesSameLabel(@Autowired FriendRepository repository) { - long friendId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:Friend{name:'Freddie'})," - + " (n)-[:KNOWS{since: 1995}]->(:Friend{name:'Frank'})" - + "RETURN n").single().get("n").asNode())); + long friendId = TestIdentitySupport.getInternalId(doWithSession(session -> session + .run("CREATE (n:Friend{name:'Freddie'})," + " (n)-[:KNOWS{since: 1995}]->(:Friend{name:'Frank'})" + + "RETURN n") + .single() + .get("n") + .asNode())); Friend person = repository.findById(friendId).get(); @@ -1605,24 +2122,24 @@ class RepositoryIT { Club club = new Club(); club.setName("BlubbClub"); WorksInClubRelationship worksInClub = new WorksInClubRelationship(2002, club); - PersonWithRelationshipWithProperties person = - new PersonWithRelationshipWithProperties("Freddie clone", hobbies, worksInClub); + PersonWithRelationshipWithProperties person = new PersonWithRelationshipWithProperties("Freddie clone", + hobbies, worksInClub); // when PersonWithRelationshipWithProperties shouldBeDifferentPerson = repository.save(person); // then assertThat(shouldBeDifferentPerson).isNotNull() - .usingRecursiveComparison() - .ignoringFieldsMatchingRegexes("^(?:(?!hobbies).)*$") - .isEqualTo(person); + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("^(?:(?!hobbies).)*$") + .isEqualTo(person); assertThat(shouldBeDifferentPerson.getName()).isEqualToIgnoringCase("Freddie clone"); assertWithSession(session -> { Record record = session.run( - "MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) RETURN n, [(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, [(n) -[r:LIKES]->(:Hobby) |r] as rels") - .single(); + "MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) RETURN n, [(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, [(n) -[r:LIKES]->(:Hobby) |r] as rels") + .single(); assertThat(record.containsKey("n")).isTrue(); assertThat(record.containsKey("Hobbies")).isTrue(); @@ -1632,15 +2149,15 @@ class RepositoryIT { assertThat(record.get("rels").values()).hasSize(2); assertThat(record.get("rels").values(Value::asRelationship)) - .extracting(Relationship::type, rel -> rel.get("active"), rel -> rel.get("localDate"), - rel -> rel.get("point"), rel -> rel.get("myEnum"), rel -> rel.get("since")) - .containsExactlyInAnyOrder( - tuple("LIKES", Values.value(rel1Active), Values.value(rel1LocalDate), - Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()), - Values.value(rel1MyEnum.name()), Values.value(rel1Since)), - tuple("LIKES", Values.value(rel2Active), Values.value(rel2LocalDate), - Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()), - Values.value(rel2MyEnum.name()), Values.value(rel2Since))); + .extracting(Relationship::type, rel -> rel.get("active"), rel -> rel.get("localDate"), + rel -> rel.get("point"), rel -> rel.get("myEnum"), rel -> rel.get("since")) + .containsExactlyInAnyOrder( + tuple("LIKES", Values.value(rel1Active), Values.value(rel1LocalDate), + Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()), + Values.value(rel1MyEnum.name()), Values.value(rel1Since)), + tuple("LIKES", Values.value(rel2Active), Values.value(rel2LocalDate), + Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()), + Values.value(rel2MyEnum.name()), Values.value(rel2Since))); }); } @@ -1648,13 +2165,15 @@ class RepositoryIT { void findEntityWithRelationshipWithPropertiesFromCustomQuery( @Autowired PersonWithRelationshipWithPropertiesRepository repository) { - Record record = doWithSession(session -> session.run(""" - CREATE - (n:PersonWithRelationshipWithProperties{name:'Freddie'}), - (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), - (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) - RETURN n, h1, h2 - """).single()); + Record record = doWithSession(session -> session + .run(""" + CREATE + (n:PersonWithRelationshipWithProperties{name:'Freddie'}), + (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), + (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) + RETURN n, h1, h2 + """) + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1697,8 +2216,11 @@ class RepositoryIT { void loadEntityWithRelationshipWithPropertiesFromCustomQueryIncoming( @Autowired HobbyWithRelationshipWithPropertiesRepository repository) { - long personId = TestIdentitySupport.getInternalId(doWithSession( - session -> session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single().get("n").asNode())); + long personId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run( + "CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1") + .single() + .get("n") + .asNode())); AltHobby hobby = repository.loadFromCustomQuery(personId); assertThat(hobby.getName()).isEqualTo("Music"); @@ -1712,10 +2234,14 @@ class RepositoryIT { @Test void loadSameNodeWithDoubleRelationship(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) { - long personId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:AltPerson{name:'Freddie'})," + - " (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," + - " (n)-[l2:LIKES {rating: 1}]->(h1)" + - " RETURN n, h1").single().get("n").asNode())); + long personId = TestIdentitySupport.getInternalId( + doWithSession(session -> session + .run("CREATE (n:AltPerson{name:'Freddie'})," + + " (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," + + " (n)-[l2:LIKES {rating: 1}]->(h1)" + " RETURN n, h1") + .single() + .get("n") + .asNode())); AltHobby hobby = repository.loadFromCustomQuery(personId); assertThat(hobby.getName()).isEqualTo("Music"); @@ -1747,9 +2273,12 @@ class RepositoryIT { void findAndMapMultipleLevelRelationshipProperties( @Autowired EntityWithRelationshipPropertiesPathRepository repository) { - long eId = doWithSession(session -> session.run("CREATE (n:EntityWithRelationshipPropertiesPath)-[:RelationshipA]->(:EntityA)" + - "-[:RelationshipB]->(:EntityB)" + - " RETURN id(n) as eId").single().get("eId").asLong()); + long eId = doWithSession(session -> session + .run("CREATE (n:EntityWithRelationshipPropertiesPath)-[:RelationshipA]->(:EntityA)" + + "-[:RelationshipB]->(:EntityB)" + " RETURN id(n) as eId") + .single() + .get("eId") + .asLong()); EntityWithRelationshipPropertiesPath entity = repository.findById(eId).get(); assertThat(entity).isNotNull(); @@ -1760,10 +2289,14 @@ class RepositoryIT { } @Test - void updateAndCreateRelationshipProperties(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) { + void updateAndCreateRelationshipProperties( + @Autowired HobbyWithRelationshipWithPropertiesRepository repository) { - long hobbyId = doWithSession( - session -> TestIdentitySupport.getInternalId(session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single().get("h1").asNode())); + long hobbyId = doWithSession(session -> TestIdentitySupport.getInternalId(session.run( + "CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1") + .single() + .get("h1") + .asNode())); AltHobby hobby = repository.findById(hobbyId).get(); assertThat(hobby.getName()).isEqualTo("Music"); @@ -1779,6 +2312,7 @@ class RepositoryIT { AltHobby savedHobby = repository.findById(hobbyId).get(); assertThat(savedHobby.getLikedBy()).hasSize(2); } + } @Nested @@ -1787,33 +2321,40 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized()); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ, "createdAt", createdAt) - ).next().get(0).asLong(); - transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); + RepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ, "createdAt", createdAt)) + .next() + .get(0) + .asLong(); + transaction + .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); IntStream.rangeClosed(1, 20) - .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", - Values.parameters("i", String.format("%02d", i)))); + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); + RepositoryIT.this.person1 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, + TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", + Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); } @Test void saveSingleEntity(@Autowired PersonRepository repository) { - PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, - LocalDate.of(1946, 9, 15), null, Arrays.asList("b", "a"), null, null); + PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, + 1509L, LocalDate.of(1946, 9, 15), null, Arrays.asList("b", "a"), null, null); PersonWithAllConstructor savedPerson = repository.save(person); assertWithSession(session -> { - Record record = session.run("MATCH (n:PersonWithAllConstructor) WHERE n.first_name = $first_name RETURN n", - Values.parameters("first_name", "Freddie")).single(); + Record record = session + .run("MATCH (n:PersonWithAllConstructor) WHERE n.first_name = $first_name RETURN n", + Values.parameters("first_name", "Freddie")) + .single(); assertThat(record.containsKey("n")).isTrue(); Node node = record.get("n").asNode(); @@ -1826,9 +2367,9 @@ class RepositoryIT { void saveNewEntityWithGeneratedIdShouldNotIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})-[r:KNOWS]->(:SimplePerson) return id(r) as rId").consume())); + doWithSession(session -> session.executeWrite(tx -> tx.run( + "CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .consume())); ThingWithFixedGeneratedId thing = new ThingWithFixedGeneratedId("name"); // this will create a duplicated relationship because we use the same ids @@ -1837,10 +2378,13 @@ class RepositoryIT { // ensure that no relationship got deleted upfront assertWithSession(session -> { - Long relCount = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") - .next().get("rCount").asLong()); + Long relCount = session + .executeRead(tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") + .next() + .get("rCount") + .asLong()); assertThat(relCount).isEqualTo(2); }); @@ -1850,19 +2394,25 @@ class RepositoryIT { void updateEntityWithGeneratedIdShouldIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - Long rId = doWithSession(session -> session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong())); + Long rId = doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong())); ThingWithFixedGeneratedId loadedThing = repository.findById("ThingWithFixedGeneratedId").get(); repository.save(loadedThing); assertWithSession(session -> { - Long newRid = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()); + Long newRid = session.executeRead( + tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong()); assertThat(rId).isNotEqualTo(newRid); }); @@ -1872,10 +2422,11 @@ class RepositoryIT { void saveAllNewEntityWithGeneratedIdShouldNotIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId").consume())); + doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .consume())); ThingWithFixedGeneratedId thing = new ThingWithFixedGeneratedId("name"); // this will create a duplicated relationship because we use the same ids @@ -1884,10 +2435,13 @@ class RepositoryIT { // ensure that no relationship got deleted upfront assertWithSession(session -> { - Long relCount = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") - .next().get("rCount").asLong()); + Long relCount = session + .executeRead(tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") + .next() + .get("rCount") + .asLong()); assertThat(relCount).isEqualTo(2); }); @@ -1897,19 +2451,25 @@ class RepositoryIT { void updateAllEntityWithGeneratedIdShouldIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - Long rId = doWithSession(session -> session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong())); + Long rId = doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong())); ThingWithFixedGeneratedId loadedThing = repository.findById("ThingWithFixedGeneratedId").get(); repository.saveAll(Collections.singletonList(loadedThing)); assertWithSession(session -> { - Long newRid = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()); + Long newRid = session.executeRead( + tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong()); assertThat(rId).isNotEqualTo(newRid); }); @@ -1921,15 +2481,16 @@ class RepositoryIT { PersonWithAllConstructor newPerson = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); - PersonWithAllConstructor existingPerson = repository.findById(id1).get(); + PersonWithAllConstructor existingPerson = repository.findById(RepositoryIT.this.id1).get(); existingPerson.setFirstName("Updated first name"); existingPerson.setNullable("Updated nullable field"); assertThat(repository.count()).isEqualTo(1); List ids = StreamSupport - .stream(repository.saveAll(Arrays.asList(existingPerson, newPerson)).spliterator(), false) - .map(PersonWithAllConstructor::getId).collect(Collectors.toList()); + .stream(repository.saveAll(Arrays.asList(existingPerson, newPerson)).spliterator(), false) + .map(PersonWithAllConstructor::getId) + .collect(Collectors.toList()); assertThat(repository.count()).isEqualTo(2); @@ -1937,7 +2498,8 @@ class RepositoryIT { Record record = session.run( "MATCH (n:PersonWithAllConstructor) WHERE id(n) IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", - Values.parameters("ids", ids)).single(); + Values.parameters("ids", ids)) + .single(); assertThat(record.containsKey("names")).isTrue(); List names = record.get("names").asList(Value::asString); @@ -1948,7 +2510,7 @@ class RepositoryIT { @Test void updateSingleEntity(@Autowired PersonRepository repository) { - PersonWithAllConstructor originalPerson = repository.findById(id1).get(); + PersonWithAllConstructor originalPerson = repository.findById(RepositoryIT.this.id1).get(); originalPerson.setFirstName("Updated first name"); originalPerson.setNullable("Updated nullable field"); assertThat(originalPerson.getThings()).isNotEmpty(); @@ -1958,8 +2520,9 @@ class RepositoryIT { assertWithSession(session -> { session.executeRead(tx -> { Record record = tx - .run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", Values.parameters("id", id1)) - .single(); + .run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", + Values.parameters("id", RepositoryIT.this.id1)) + .single(); assertThat(record.containsKey("n")).isTrue(); Node node = record.get("n").asNode(); @@ -1983,7 +2546,8 @@ class RepositoryIT { assertWithSession(session -> { Record record = session - .run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", thing.getTheId())).single(); + .run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", thing.getTheId())) + .single(); assertThat(record.containsKey("n")).isTrue(); Node node = record.get("n").asNode(); @@ -2007,10 +2571,10 @@ class RepositoryIT { repository.saveAll(Arrays.asList(newThing, existingThing)); assertWithSession(session -> { - Record record = session - .run("MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", - Values.parameters("ids", Arrays.asList(newThing.getTheId(), existingThing.getTheId()))) - .single(); + Record record = session.run( + "MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", + Values.parameters("ids", Arrays.asList(newThing.getTheId(), existingThing.getTheId()))) + .single(); assertThat(record.containsKey("names")).isTrue(); List names = record.get("names").asList(Value::asString); @@ -2032,10 +2596,10 @@ class RepositoryIT { repository.save(thing); assertWithSession(session -> { - Record record = session - .run("MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", - Values.parameters("ids", Arrays.asList("id07", "id15"))) - .single(); + Record record = session.run( + "MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", + Values.parameters("ids", Arrays.asList("id07", "id15"))) + .single(); assertThat(record.containsKey("names")).isTrue(); List names = record.get("names").asList(Value::asString); @@ -2072,7 +2636,8 @@ class RepositoryIT { @Test // DATAGRAPH-1452 void createWithCustomQueryShouldWorkWithPlainObjects(@Autowired PersonRepository repository) { - PersonWithAllConstructor p = new PersonWithAllConstructor(null, "NewName", "NewFirstName", null, null, null, LocalDate.now(), null, null, null, null); + PersonWithAllConstructor p = new PersonWithAllConstructor(null, "NewName", "NewFirstName", null, null, null, + LocalDate.now(), null, null, null, null); PersonWithAllConstructor newPerson = repository.createWithCustomQuery(p); assertThat(newPerson.getName()).isEqualTo(p.getName()); @@ -2120,7 +2685,8 @@ class RepositoryIT { } @Test // DATAGRAPH-2292 - void createWithCustomQueryShouldWorkWithCollectionsOfNestedObjects(@Autowired RelationshipRepository repository) { + void createWithCustomQueryShouldWorkWithCollectionsOfNestedObjects( + @Autowired RelationshipRepository repository) { Assumptions.assumeTrue(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v4_1_0)); @@ -2130,14 +2696,13 @@ class RepositoryIT { people.add(createNewPerson("Another person", c27)); List newPeople = repository.createManyWithCustomQuery(people); - assertThat(newPeople).hasSize(2) - .allSatisfy(p -> { - PersonWithRelationship newPerson = repository.findById(p.getId()).get(); - assertThat(newPerson.getName()).isEqualTo(p.getName()); - assertThat(newPerson.getHobbies().getName()).isEqualTo("A Hobby"); - assertThat(newPerson.getPets()).extracting(Pet::getName).containsExactlyInAnyOrder("A", "B"); - assertThat(newPerson.getClub().getName()).isEqualTo("C27"); - }); + assertThat(newPeople).hasSize(2).allSatisfy(p -> { + PersonWithRelationship newPerson = repository.findById(p.getId()).get(); + assertThat(newPerson.getName()).isEqualTo(p.getName()); + assertThat(newPerson.getHobbies().getName()).isEqualTo("A Hobby"); + assertThat(newPerson.getPets()).extracting(Pet::getName).containsExactlyInAnyOrder("A", "B"); + assertThat(newPerson.getClub().getName()).isEqualTo("C27"); + }); } @Test @@ -2161,12 +2726,12 @@ class RepositoryIT { assertWithSession(session -> { Record record = session.run(""" - MATCH (n:PersonWithRelationship) - RETURN - n, - [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies, - [(n)-[:Has]->(h:Hobby) | h] as hobbies, [(n)<-[:Has]-(c:Club) | c] as clubs - """, Values.parameters("name", "Freddie")).single(); + MATCH (n:PersonWithRelationship) + RETURN + n, + [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies, + [(n)-[:Has]->(h:Hobby) | h] as hobbies, [(n)<-[:Has]-(c:Club) | c] as clubs + """, Values.parameters("name", "Freddie")).single(); assertThat(record.containsKey("n")).isTrue(); Node rootNode = record.get("n").asNode(); @@ -2180,20 +2745,21 @@ class RepositoryIT { pets.put(petWithHobbies.get(0), ((List) petWithHobbies.get(1))); } - assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("Jerry", "Tom"); + assertThat(pets.keySet() + .stream() + .map(pet -> ((Node) pet).get("name").asString()) + .collect(Collectors.toList())).containsExactlyInAnyOrder("Jerry", "Tom"); - assertThat(pets.values().stream() - .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("sleeping"); + assertThat(pets.values() + .stream() + .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())) + .collect(Collectors.toList())).containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); assertThat(record.get("clubs").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("ClownsClub"); + .containsExactlyInAnyOrder("ClownsClub"); }); } @@ -2241,17 +2807,18 @@ class RepositoryIT { pets.put(petWithHobbies.get(0), ((List) petWithHobbies.get(1))); } - assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("Jerry", "Tom"); + assertThat(pets.keySet() + .stream() + .map(pet -> ((Node) pet).get("name").asString()) + .collect(Collectors.toList())).containsExactlyInAnyOrder("Jerry", "Tom"); - assertThat(pets.values().stream() - .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("sleeping"); + assertThat(pets.values() + .stream() + .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())) + .collect(Collectors.toList())).containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); // assert that only two hobbies is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); @@ -2266,7 +2833,10 @@ class RepositoryIT { @Test void saveEntityWithAlreadyExistingTargetNode(@Autowired RelationshipRepository repository) { - Long hobbyId = doWithSession(session -> session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId").single().get("hId").asLong()); + Long hobbyId = doWithSession(session -> session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId") + .single() + .get("hId") + .asLong()); PersonWithRelationship person = new PersonWithRelationship(); person.setName("Freddie"); @@ -2279,9 +2849,9 @@ class RepositoryIT { assertWithSession(session -> { List recordList = session - .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", - Values.parameters("name", "Freddie")) - .list(); + .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", + Values.parameters("name", "Freddie")) + .list(); assertThat(recordList).hasSize(1); @@ -2293,7 +2863,7 @@ class RepositoryIT { assertThat(savedPerson.getName()).isEqualTo("Freddie"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); // assert that only one hobby is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); @@ -2306,7 +2876,7 @@ class RepositoryIT { Record ids = doWithSession(session -> session.run( "CREATE (p:PersonWithRelationship{name: 'Freddie'}), (h:Hobby{name: 'Music'}) return id(h) as hId, id(p) as pId") - .single()); + .single()); long personId = ids.get("pId").asLong(); long hobbyId = ids.get("hId").asLong(); @@ -2323,8 +2893,9 @@ class RepositoryIT { assertWithSession(session -> { List recordList = session - .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", Values.parameters("name", "Freddie")) - .list(); + .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", + Values.parameters("name", "Freddie")) + .list(); assertThat(recordList).hasSize(1); @@ -2336,7 +2907,7 @@ class RepositoryIT { assertThat(savedPerson.getName()).isEqualTo("Freddie"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); // assert that only one hobby is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); @@ -2358,11 +2929,13 @@ class RepositoryIT { repository.save(rootPet); assertWithSession(session -> { - Record record = session.run(""" - MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)-[:Has]->(petOfGrandChildPet:Pet) - RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet - """, - Collections.emptyMap()).single(); + Record record = session + .run(""" + MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)-[:Has]->(petOfGrandChildPet:Pet) + RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet + """, + Collections.emptyMap()) + .single(); assertThat(record.get("rootPet").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("petOfRootPet").asNode().get("name").asString()).isEqualTo("Daphne"); @@ -2382,10 +2955,12 @@ class RepositoryIT { repository.save(luna); assertWithSession(session -> { - Record record = session.run(""" - MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(luna2:Pet{name:'Luna'}) - RETURN luna, daphne, luna2 - """).single(); + Record record = session + .run(""" + MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(luna2:Pet{name:'Luna'}) + RETURN luna, daphne, luna2 + """) + .single(); assertThat(record.get("luna").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("daphne").asNode().get("name").asString()).isEqualTo("Daphne"); @@ -2403,7 +2978,9 @@ class RepositoryIT { repository.save(originalThing); assertWithSession(session -> { - Record record = session.run("MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'}) RETURN r").single(); + Record record = session.run( + "MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'}) RETURN r") + .single(); assertThat(record.keys()).isNotEmpty(); assertThat(record.containsKey("r")).isTrue(); @@ -2421,8 +2998,10 @@ class RepositoryIT { ThingWithAssignedId savedThing = repository.save(thing); assertWithSession(session -> { - Record record = session.run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t", - Values.parameters("id", savedThing.getTheId())).single(); + Record record = session + .run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t", + Values.parameters("id", savedThing.getTheId())) + .single(); assertThat(record.containsKey("n")).isTrue(); assertThat(record.containsKey("t")).isTrue(); @@ -2447,8 +3026,10 @@ class RepositoryIT { repository.saveAll(Collections.singletonList(thing)); assertWithSession(session -> { - Record record = session.run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t", - Values.parameters("id", thing.getTheId())).single(); + Record record = session + .run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t", + Values.parameters("id", thing.getTheId())) + .single(); assertThat(record.containsKey("n")).isTrue(); assertThat(record.containsKey("t")).isTrue(); @@ -2491,8 +3072,10 @@ class RepositoryIT { repository.save(start); assertWithSession(session -> { - List records = session.run("MATCH (end:BidirectionalEnd)<-[r:CONNECTED]-(start:BidirectionalStart)" + - " RETURN start, r, end").list(); + List records = session + .run("MATCH (end:BidirectionalEnd)<-[r:CONNECTED]-(start:BidirectionalStart)" + + " RETURN start, r, end") + .list(); assertThat(records).hasSize(1); }); @@ -2503,10 +3086,10 @@ class RepositoryIT { BidirectionalSameEntity entity1 = new BidirectionalSameEntity("e1"); BidirectionalSameEntity entity2 = new BidirectionalSameEntity("e2"); - BidirectionalSameEntity.BidirectionalSameRelationship e1KnowsE2 = - new BidirectionalSameEntity.BidirectionalSameRelationship(entity2); - BidirectionalSameEntity.BidirectionalSameRelationship e2KnowsE1 = - new BidirectionalSameEntity.BidirectionalSameRelationship(entity1); + BidirectionalSameEntity.BidirectionalSameRelationship e1KnowsE2 = new BidirectionalSameEntity.BidirectionalSameRelationship( + entity2); + BidirectionalSameEntity.BidirectionalSameRelationship e2KnowsE1 = new BidirectionalSameEntity.BidirectionalSameRelationship( + entity1); entity1.setKnows(Collections.singletonList(e1KnowsE2)); entity2.setKnows(Collections.singletonList(e2KnowsE1)); @@ -2515,20 +3098,21 @@ class RepositoryIT { assertWithSession(session -> { List records = session.run( "MATCH (e:BidirectionalSameEntity{id:'e1'})-[:KNOWS]->(:BidirectionalSameEntity{id:'e2'}) RETURN e") - .list(); + .list(); assertThat(records).hasSize(1); records = session.run( "MATCH (e:BidirectionalSameEntity{id:'e2'})-[:KNOWS]->(:BidirectionalSameEntity{id:'e1'}) RETURN e") - .list(); + .list(); assertThat(records).hasSize(1); }); } @Test // GH-2240 - void saveBidirectionalRelationshipsWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) { + void saveBidirectionalRelationshipsWithExternallyGeneratedId( + @Autowired BidirectionalExternallyGeneratedIdRepository repository) { BidirectionalExternallyGeneratedId a = new BidirectionalExternallyGeneratedId(); BidirectionalExternallyGeneratedId b = new BidirectionalExternallyGeneratedId(); @@ -2573,71 +3157,70 @@ class RepositoryIT { @Autowired SameIdEntitiesWithRelationshipPropertiesRepository repository) { List routes = new ArrayList<>(); - routes.add(new SameIdProperty.RouteProperties() - .withPod(new SameIdProperty.PodEntity() - .withCode("BEANR") - ) - .withTruck(20d)); + routes.add(new SameIdProperty.RouteProperties().withPod(new SameIdProperty.PodEntity().withCode("BEANR")) + .withTruck(20d)); - routes.add(new SameIdProperty.RouteProperties() - .withPod(new SameIdProperty.PodEntity() - .withCode("TRMER") // Here is the duplicated, but for another kind of node. - ) - .withTruck(20d)); + routes.add(new SameIdProperty.RouteProperties().withPod(new SameIdProperty.PodEntity().withCode("TRMER") // Here + // is + // the + // duplicated, + // but + // for + // another + // kind + // of + // node. + ).withTruck(20d)); SameIdProperty.PolEntityWithRelationshipProperties polEntity = new SameIdProperty.PolEntityWithRelationshipProperties() - .withCode("TRMER") - .withRoutes(routes); + .withCode("TRMER") + .withRoutes(routes); repository.save(polEntity); assertWithSession(session -> { - List list = session.run( - "MATCH (pol:PolWithRP{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'TRMER'}) return pol, pod" - ).list(); + List list = session + .run("MATCH (pol:PolWithRP{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'TRMER'}) return pol, pod") + .list(); assertThat(list).hasSize(1); - list = session.run( - "MATCH (pol:PolWithRP{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'BEANR'}) return pol, pod" - ).list(); + list = session + .run("MATCH (pol:PolWithRP{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'BEANR'}) return pol, pod") + .list(); assertThat(list).hasSize(1); - list = session.run( - "MATCH (pod1:Pod{code:'TRMER'})-[:ROUTES]->(pod2:Pod{code:'TRMER'}) return pod1, pod2" - ).list(); + list = session + .run("MATCH (pod1:Pod{code:'TRMER'})-[:ROUTES]->(pod2:Pod{code:'TRMER'}) return pod1, pod2") + .list(); assertThat(list).hasSize(0); }); } @Test // GH-2108 - void saveRelatedEntitiesWithSameCustomIdsAndPlainRelationships( - @Autowired SameIdEntitiesRepository repository) { + void saveRelatedEntitiesWithSameCustomIdsAndPlainRelationships(@Autowired SameIdEntitiesRepository repository) { List routes = new ArrayList<>(); routes.add(new SameIdProperty.PodEntity().withCode("BEANR")); routes.add(new SameIdProperty.PodEntity().withCode("TRMER")); - SameIdProperty.PolEntity polEntity = new SameIdProperty.PolEntity() - .withCode("TRMER") - .withRoutes(routes); + SameIdProperty.PolEntity polEntity = new SameIdProperty.PolEntity().withCode("TRMER").withRoutes(routes); repository.save(polEntity); assertWithSession(session -> { - List list = session.run( - "MATCH (pol:Pol{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'TRMER'}) return pol, pod" - ).list(); + List list = session + .run("MATCH (pol:Pol{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'TRMER'}) return pol, pod") + .list(); assertThat(list).hasSize(1); - list = session.run( - "MATCH (pol:Pol{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'BEANR'}) return pol, pod" - ).list(); + list = session.run("MATCH (pol:Pol{code:'TRMER'})-[:ROUTES]->(pod:Pod{code:'BEANR'}) return pol, pod") + .list(); assertThat(list).hasSize(1); - list = session.run( - "MATCH (pod1:Pod{code:'TRMER'})-[:ROUTES]->(pod2:Pod{code:'TRMER'}) return pod1, pod2" - ).list(); + list = session + .run("MATCH (pod1:Pod{code:'TRMER'})-[:ROUTES]->(pod2:Pod{code:'TRMER'}) return pod1, pod2") + .list(); assertThat(list).hasSize(0); }); } @@ -2668,6 +3251,7 @@ class RepositoryIT { assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2); } + } @Nested @@ -2675,29 +3259,41 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { - id1 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON1_NAME)).next().get(0).asLong(); - id2 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON2_NAME)).next().get(0).asLong(); + RepositoryIT.this.id1 = transaction + .run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", + Collections.singletonMap("name", TEST_PERSON1_NAME)) + .next() + .get(0) + .asLong(); + RepositoryIT.this.id2 = transaction + .run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", + Collections.singletonMap("name", TEST_PERSON2_NAME)) + .next() + .get(0) + .asLong(); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, null, null, null, null, null, null, null, null, null); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, null, null, null, null, null, null, null, null, null); + RepositoryIT.this.person1 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, null, + null, null, null, null, null, null, null, null); + RepositoryIT.this.person2 = new PersonWithAllConstructor(RepositoryIT.this.id2, TEST_PERSON2_NAME, null, + null, null, null, null, null, null, null, null); } @Test void delete(@Autowired PersonRepository repository) { - repository.delete(person1); + repository.delete(RepositoryIT.this.person1); - assertThat(repository.existsById(id1)).isFalse(); - assertThat(repository.existsById(id2)).isTrue(); + assertThat(repository.existsById(RepositoryIT.this.id1)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id2)).isTrue(); } @Test void deleteById(@Autowired PersonRepository repository) { - repository.deleteById(id1); + repository.deleteById(RepositoryIT.this.id1); - assertThat(repository.existsById(id1)).isFalse(); - assertThat(repository.existsById(id2)).isTrue(); + assertThat(repository.existsById(RepositoryIT.this.id1)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id2)).isTrue(); } @Test // GH-2281 @@ -2705,8 +3301,8 @@ class RepositoryIT { repository.deleteAllByName(TEST_PERSON1_NAME); - assertThat(repository.existsById(id1)).isFalse(); - assertThat(repository.existsById(id2)).isTrue(); + assertThat(repository.existsById(RepositoryIT.this.id1)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id2)).isTrue(); } @Test // GH-2281 @@ -2715,31 +3311,32 @@ class RepositoryIT { long deleted = repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME); assertThat(deleted).isEqualTo(2L); - assertThat(repository.existsById(id1)).isFalse(); - assertThat(repository.existsById(id2)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id1)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id2)).isFalse(); } @Test void deleteAllEntities(@Autowired PersonRepository repository) { - repository.deleteAll(Arrays.asList(person1, person2)); + repository.deleteAll(Arrays.asList(RepositoryIT.this.person1, RepositoryIT.this.person2)); - assertThat(repository.existsById(id1)).isFalse(); - assertThat(repository.existsById(id2)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id1)).isFalse(); + assertThat(repository.existsById(RepositoryIT.this.id2)).isFalse(); } @Test // DATAGRAPH-1428 void deleteAllById(@Autowired PersonRepository repository) { - PersonWithAllConstructor person3 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, - TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, - Instant.now()); + PersonWithAllConstructor person3 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, + TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", + Arrays.asList("a", "b"), NEO4J_HQ, Instant.now()); repository.save(person3); - repository.deleteAllById(Arrays.asList(person1.getId(), person3.getId())); + repository.deleteAllById(Arrays.asList(RepositoryIT.this.person1.getId(), person3.getId())); - assertThat(repository.findAll()).extracting(PersonWithAllConstructor::getId).containsExactly(id2); + assertThat(repository.findAll()).extracting(PersonWithAllConstructor::getId) + .containsExactly(RepositoryIT.this.id2); } @Test @@ -2751,7 +3348,9 @@ class RepositoryIT { @Test void deleteSimpleRelationship(@Autowired RelationshipRepository repository) { - doWithSession(session -> session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})") + .consume()); PersonWithRelationship person = repository.getPersonWithRelationshipsViaQuery(); person.setHobbies(null); @@ -2763,9 +3362,10 @@ class RepositoryIT { @Test void deleteCollectionRelationship(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'}), " - + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'}), " + + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})") + .consume()); PersonWithRelationship person = repository.getPersonWithRelationshipsViaQuery(); person.getPets().remove(0); @@ -2783,25 +3383,33 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized()); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ, "createdAt", createdAt)) - .next().get(0).asLong(); - id2 = transaction.run( + RepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ, "createdAt", createdAt)) + .next() + .get(0) + .asLong(); + RepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + RepositoryIT.this.person1 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, + TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", + Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); + RepositoryIT.this.person2 = new PersonWithAllConstructor(RepositoryIT.this.id2, TEST_PERSON2_NAME, + TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, + Collections.emptyList(), SFO, null); transaction.run(""" CREATE (lhr:Airport {code: 'LHR', name: 'London Heathrow'}) @@ -2823,87 +3431,82 @@ class RepositoryIT { @Test void findOneByExample(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); Optional person = repository.findOne(example); assertThat(person).isPresent(); - assertThat(person.get()).isEqualTo(person1); + assertThat(person.get()).isEqualTo(RepositoryIT.this.person1); } @Test // GH-2343 void findOneByExampleFluent(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); PersonWithAllConstructor person = repository.findBy(example, q -> q.oneValue()); assertThat(person).isNotNull(); - assertThat(person).isEqualTo(person1); + assertThat(person).isEqualTo(RepositoryIT.this.person1); } @Test void findAllByExample(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); List persons = repository.findAll(example); - assertThat(persons).containsExactly(person1); + assertThat(persons).containsExactly(RepositoryIT.this.person1); } @Test // GH-2343 void findAllByExampleFluent(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); List persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::all); - assertThat(persons).containsExactly(person1); + assertThat(persons).containsExactly(RepositoryIT.this.person1); } @Test // GH-2343 void findAllByExampleFluentProjecting(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); List persons = repository.findBy(example, q -> q.project("name", "firstName").all()); - assertThat(persons) - .hasSize(1) - .first().satisfies(p -> { - assertThat(p.getName()).isEqualTo(person1.getName()); - assertThat(p.getFirstName()).isEqualTo(person1.getFirstName()); - assertThat(p.getId()).isNotNull(); + assertThat(persons).hasSize(1).first().satisfies(p -> { + assertThat(p.getName()).isEqualTo(RepositoryIT.this.person1.getName()); + assertThat(p.getFirstName()).isEqualTo(RepositoryIT.this.person1.getFirstName()); + assertThat(p.getId()).isNotNull(); - assertThat(p.getBornOn()).isNull(); - assertThat(p.getCool()).isNull(); - assertThat(p.getCreatedAt()).isNull(); - assertThat(p.getNullable()).isNull(); - assertThat(p.getPersonNumber()).isNull(); - assertThat(p.getPlace()).isNull(); - assertThat(p.getSameValue()).isNull(); - assertThat(p.getThings()).isNull(); - }); + assertThat(p.getBornOn()).isNull(); + assertThat(p.getCool()).isNull(); + assertThat(p.getCreatedAt()).isNull(); + assertThat(p.getNullable()).isNull(); + assertThat(p.getPersonNumber()).isNull(); + assertThat(p.getPlace()).isNull(); + assertThat(p.getSameValue()).isNull(); + assertThat(p.getThings()).isNull(); + }); } @Test void findAllByExampleFluentProjectingRelationships(@Autowired FlightRepository repository) { Example example = Example.of(new Flight("FL 001", null, null), ExampleMatcher.matchingAll().withIgnoreNullValues()); - List flights = repository.findBy(example, - q -> q.project("name", "departure.name").all()); + List flights = repository.findBy(example, q -> q.project("name", "departure.name").all()); - assertThat(flights) - .hasSize(1) - .first().satisfies(p -> { - assertThat(p.getName()).isEqualTo("FL 001"); - assertThat(p.getArrival()).isNull(); - assertThat(p.getDeparture()).isNotNull(); - assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); - assertThat(p.getDeparture().getCode()).isNull(); - }); + assertThat(flights).hasSize(1).first().satisfies(p -> { + assertThat(p.getName()).isEqualTo("FL 001"); + assertThat(p.getArrival()).isNull(); + assertThat(p.getDeparture()).isNotNull(); + assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); + assertThat(p.getDeparture().getCode()).isNull(); + }); } @Test @@ -2913,64 +3516,66 @@ class RepositoryIT { List flights = repository.findBy(example, q -> q.project("name", "nextFlight.name", "nextFlight.nextFlight.name").all()); - assertThat(flights) - .hasSize(1) - .first().satisfies(p -> { - assertThat(p.getName()).isEqualTo("FL 001"); - assertThat(p.getNextFlight().getName()).isEqualTo("FL 002"); - assertThat(p.getNextFlight().getNextFlight().getName()).isEqualTo("FL 003"); - }); + assertThat(flights).hasSize(1).first().satisfies(p -> { + assertThat(p.getName()).isEqualTo("FL 001"); + assertThat(p.getNextFlight().getName()).isEqualTo("FL 002"); + assertThat(p.getNextFlight().getNextFlight().getName()).isEqualTo("FL 003"); + }); } @Test // GH-2343 void findAllByExampleFluentAs(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); List people = repository.findBy(example, q -> q.as(DtoPersonProjection.class).all()); - assertThat(people) - .hasSize(1) - .extracting(DtoPersonProjection::getFirstName) - .first().isEqualTo(TEST_PERSON1_FIRST_NAME); + assertThat(people).hasSize(1) + .extracting(DtoPersonProjection::getFirstName) + .first() + .isEqualTo(TEST_PERSON1_FIRST_NAME); } @Test // GH-2343 void streamByExample(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); - Stream persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::stream); + Stream persons = repository.findBy(example, + FluentQuery.FetchableFluentQuery::stream); - assertThat(persons).containsExactly(person1); + assertThat(persons).containsExactly(RepositoryIT.this.person1); } @Test // GH-2343 void findFirstByExample(@Autowired PersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(RepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); - PersonWithAllConstructor person = repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).firstValue()); + PersonWithAllConstructor person = repository.findBy(example, + q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).firstValue()); assertThat(person).isNotNull(); - assertThat(person).isEqualTo(person1); + assertThat(person).isEqualTo(RepositoryIT.this.person1); } @Test // GH-2726 void scrollByExample(@Autowired PersonRepository repository) { - PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); + PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, + TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); Example example = Example.of(sameValuePerson, ExampleMatcher.matchingAll().withIgnoreNullValues()); - Window person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset())); + Window person = repository.findBy(example, + q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset())); assertThat(person).isNotNull(); - assertThat(person.getContent().get(0)).isEqualTo(person1); + assertThat(person.getContent().get(0)).isEqualTo(RepositoryIT.this.person1); - ScrollPosition currentPosition = person.positionAt(person1); + ScrollPosition currentPosition = person.positionAt(RepositoryIT.this.person1); person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition)); - assertThat(person.getContent().get(0)).isEqualTo(person2); + assertThat(person.getContent().get(0)).isEqualTo(RepositoryIT.this.person2); } @Test @@ -2980,38 +3585,39 @@ class RepositoryIT { Example example; List persons; - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, - null, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, + null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); persons = repository.findAll(example); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, null, - null, null, null, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, + null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); persons = repository.findAll(example); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), - TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null, null, - null, null); + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, + null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAll() - .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true)) - .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.STARTING)) - .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.CONTAINING))); + .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true)) + .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.STARTING)) + .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.CONTAINING))); persons = repository.findAll(example); - assertThat(persons).containsExactlyInAnyOrder(person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person2); - person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null, null); + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null, + null); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX)); persons = repository.findAll(example); - assertThat(persons).containsExactlyInAnyOrder(person1); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX).withIncludeNullValues()); @@ -3026,35 +3632,37 @@ class RepositoryIT { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); List persons = repository.findAll(example, Sort.by(Sort.Direction.DESC, "name")); - assertThat(persons).containsExactly(person2, person1); + assertThat(persons).containsExactly(RepositoryIT.this.person2, RepositoryIT.this.person1); } @Test // GH-2343 void findAllByExampleWithSortFluent(@Autowired PersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); - List persons = repository - .findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()); + List persons = repository.findBy(example, + q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()); - assertThat(persons).containsExactly(person2, person1); + assertThat(persons).containsExactly(RepositoryIT.this.person2, RepositoryIT.this.person1); } @Test void findAllByExampleWithPagination(@Autowired PersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); - Iterable persons = repository.findAll(example, PageRequest.of(1, 1, Sort.by("name"))); + Iterable persons = repository.findAll(example, + PageRequest.of(1, 1, Sort.by("name"))); - assertThat(persons).containsExactly(person2); + assertThat(persons).containsExactly(RepositoryIT.this.person2); } @Test // GH-2343 void findAllByExampleWithPaginationFluent(@Autowired PersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); - Iterable persons = repository.findBy(example, q -> q.page(PageRequest.of(1, 1, Sort.by("name")))); + Iterable persons = repository.findBy(example, + q -> q.page(PageRequest.of(1, 1, Sort.by("name")))); - assertThat(persons).containsExactly(person2); + assertThat(persons).containsExactly(RepositoryIT.this.person2); } @Test // GH-2726 @@ -3064,7 +3672,7 @@ class RepositoryIT { Window persons = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.keyset().forward())); - assertThat(persons.getContent()).containsExactly(person1); + assertThat(persons.getContent()).containsExactly(RepositoryIT.this.person1); } @Test @@ -3088,7 +3696,7 @@ class RepositoryIT { @Test void countByExample(@Autowired PersonRepository repository) { - Example example = Example.of(person1); + Example example = Example.of(RepositoryIT.this.person1); long count = repository.count(example); assertThat(count).isEqualTo(1); @@ -3097,7 +3705,7 @@ class RepositoryIT { @Test // GH-2343 void countByExampleFluent(@Autowired PersonRepository repository) { - Example example = Example.of(person1); + Example example = Example.of(RepositoryIT.this.person1); long count = repository.findBy(example, q -> q.count()); assertThat(count).isEqualTo(1); @@ -3106,25 +3714,27 @@ class RepositoryIT { @Test // GH-2703 void negatedProperties(@Autowired PersonRepository repository) { - var example = Example.of(new PersonWithAllConstructor(null, person1.getName(), null, null, null, null, null, null, null, null, null), + var example = Example.of( + new PersonWithAllConstructor(null, RepositoryIT.this.person1.getName(), null, null, null, null, + null, null, null, null, null), ExampleMatcher.matchingAll().withTransformer("name", Neo4jPropertyValueTransformers.notMatching())); var optionalPerson = repository.findOne(example); - assertThat(optionalPerson) - .map(PersonWithAllConstructor::getName) - .hasValue(person2.getName()); + assertThat(optionalPerson).map(PersonWithAllConstructor::getName) + .hasValue(RepositoryIT.this.person2.getName()); } @Test // GH-2703 void negatedInternalIdProperty(@Autowired PersonRepository repository) { - var example = Example.of(new PersonWithAllConstructor(person1.getId(), null, null, null, null, null, null, null, null, null, null), + var example = Example.of( + new PersonWithAllConstructor(RepositoryIT.this.person1.getId(), null, null, null, null, null, null, + null, null, null, null), ExampleMatcher.matchingAll().withTransformer("id", Neo4jPropertyValueTransformers.notMatching())); var optionalPerson = repository.findOne(example); - assertThat(optionalPerson) - .map(PersonWithAllConstructor::getName) - .hasValue(person2.getName()); + assertThat(optionalPerson).map(PersonWithAllConstructor::getName) + .hasValue(RepositoryIT.this.person2.getName()); } @Test // GH-2240 @@ -3137,17 +3747,15 @@ class RepositoryIT { ExampleMatcher.matchingAll().withTransformer("uuid", Neo4jPropertyValueTransformers.notMatching())); var optionalResult = repository.findOne(example); - assertThat(optionalResult) - .map(BidirectionalExternallyGeneratedId::getUuid) - .hasValue(b.getUuid()); + assertThat(optionalResult).map(BidirectionalExternallyGeneratedId::getUuid).hasValue(b.getUuid()); } @Test void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) RETURN n, h1, p1, p2") - .single()); + Record record = doWithSession(session -> session.run( + "CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) RETURN n, h1, p1, p2") + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -3182,15 +3790,14 @@ class RepositoryIT { @Test void findEntityWithRelationshipByFindAllByExample(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}), - (p1)-[:Has]->(p3:Pet{name: 'Silvester'})-[:Has]->(h2:Hobby{name: 'Hunt Tweety'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}), + (p1)-[:Has]->(p3:Pet{name: 'Silvester'})-[:Has]->(h2:Hobby{name: 'Hunt Tweety'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -3233,14 +3840,13 @@ class RepositoryIT { @Test void findEntityWithRelationshipByFindAllByExampleWithSort(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -3272,14 +3878,13 @@ class RepositoryIT { @Test void findEntityWithRelationshipByFindAllByExampleWithPageable(@Autowired RelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -3293,7 +3898,10 @@ class RepositoryIT { PersonWithRelationship probe = new PersonWithRelationship(); probe.setName("Freddie"); - PersonWithRelationship loadedPerson = repository.findAll(Example.of(probe), PageRequest.of(0, 1, Sort.by("name"))).toList().get(0); + PersonWithRelationship loadedPerson = repository + .findAll(Example.of(probe), PageRequest.of(0, 1, Sort.by("name"))) + .toList() + .get(0); assertThat(loadedPerson.getName()).isEqualTo("Freddie"); assertThat(loadedPerson.getId()).isEqualTo(personId); Hobby hobby = loadedPerson.getHobbies(); @@ -3316,29 +3924,37 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized()); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ, "createdAt", createdAt)) - .next().get(0).asLong(); - id2 = transaction.run( + RepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ, "createdAt", createdAt)) + .next() + .get(0) + .asLong(); + RepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); IntStream.rangeClosed(1, 20) - .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", - Values.parameters("i", String.format("%02d", i)))); + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + RepositoryIT.this.person1 = new PersonWithAllConstructor(RepositoryIT.this.id1, TEST_PERSON1_NAME, + TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", + Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); + RepositoryIT.this.person2 = new PersonWithAllConstructor(RepositoryIT.this.id2, TEST_PERSON2_NAME, + TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, + Collections.emptyList(), SFO, null); } @Test @@ -3347,10 +3963,10 @@ class RepositoryIT { List persons; persons = repository.findAllByNameNot(TEST_PERSON1_NAME); - assertThat(persons).doesNotContain(person1); + assertThat(persons).doesNotContain(RepositoryIT.this.person1); persons = repository.findAllByNameNotIgnoreCase(TEST_PERSON1_NAME.toUpperCase()); - assertThat(persons).doesNotContain(person1); + assertThat(persons).doesNotContain(RepositoryIT.this.person1); } @Test @@ -3358,8 +3974,8 @@ class RepositoryIT { List coolPeople = repository.findAllByCoolTrue(); List theRest = repository.findAllByCoolFalse(); - assertThat(coolPeople).doesNotContain(person2); - assertThat(theRest).doesNotContain(person1); + assertThat(coolPeople).doesNotContain(RepositoryIT.this.person2); + assertThat(theRest).doesNotContain(RepositoryIT.this.person1); } @Test @@ -3368,17 +3984,17 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameLike("Ern"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByFirstNameLikeIgnoreCase("eRN"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByMatches(@Autowired PersonRepository repository) { List persons = repository.findAllByFirstNameMatches("(?i)ern.*"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test @@ -3387,10 +4003,10 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameNotLike("Ern"); - assertThat(persons).doesNotContain(person1); + assertThat(persons).doesNotContain(RepositoryIT.this.person1); persons = repository.findAllByFirstNameNotLikeIgnoreCase("eRN"); - assertThat(persons).doesNotContain(person1); + assertThat(persons).doesNotContain(RepositoryIT.this.person1); } @Test @@ -3399,10 +4015,10 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameStartingWith("Er"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByFirstNameStartingWithIgnoreCase("eRN"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test @@ -3411,10 +4027,10 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameContaining("ni"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByFirstNameContainingIgnoreCase("NI"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test @@ -3423,10 +4039,10 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameNotContaining("ni"); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); persons = repository.findAllByFirstNameNotContainingIgnoreCase("NI"); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test @@ -3435,64 +4051,68 @@ class RepositoryIT { List persons; persons = repository.findAllByFirstNameEndingWith("nie"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByFirstNameEndingWithIgnoreCase("NIE"); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByLessThan(@Autowired PersonRepository repository) { List persons = repository.findAllByPersonNumberIsLessThan(2L); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByLessThanEqual(@Autowired PersonRepository repository) { List persons = repository.findAllByPersonNumberIsLessThanEqual(2L); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test void findByGreaterThanEqual(@Autowired PersonRepository repository) { List persons = repository.findAllByPersonNumberIsGreaterThanEqual(1L); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test void findByGreaterThan(@Autowired PersonRepository repository) { List persons = repository.findAllByPersonNumberIsGreaterThan(1L); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test void findByBetweenRange(@Autowired PersonRepository repository) { List persons; - persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.inclusive(1L)).to(Bound.inclusive(2L))); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + persons = repository + .findAllByPersonNumberIsBetween(Range.from(Bound.inclusive(1L)).to(Bound.inclusive(2L))); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); - persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.inclusive(1L)).to(Bound.exclusive(2L))); - assertThat(persons).hasSize(1).contains(person1); + persons = repository + .findAllByPersonNumberIsBetween(Range.from(Bound.inclusive(1L)).to(Bound.exclusive(2L))); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.inclusive(1L)).to(Bound.unbounded())); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.exclusive(1L)).to(Bound.unbounded())); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); - persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(Bound.inclusive(2L))); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + persons = repository + .findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(Bound.inclusive(2L))); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); - persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(Bound.exclusive(2L))); - assertThat(persons).hasSize(1).contains(person1); + persons = repository + .findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(Bound.exclusive(2L))); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByPersonNumberIsBetween(Range.unbounded()); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); } @Test @@ -3500,64 +4120,63 @@ class RepositoryIT { List persons; persons = repository.findAllByPersonNumberIsBetween(1L, 2L); - assertThat(persons).containsExactlyInAnyOrder(person1, person2); + assertThat(persons).containsExactlyInAnyOrder(RepositoryIT.this.person1, RepositoryIT.this.person2); persons = repository.findAllByPersonNumberIsBetween(3L, 5L); assertThat(persons).isEmpty(); persons = repository.findAllByPersonNumberIsBetween(2L, 3L); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test void findByAfter(@Autowired PersonRepository repository) { List persons = repository.findAllByBornOnAfter(TEST_PERSON1_BORN_ON); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test void findByBefore(@Autowired PersonRepository repository) { List persons = repository.findAllByBornOnBefore(TEST_PERSON2_BORN_ON); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByInstant(@Autowired PersonRepository repository) { List persons = repository - .findAllByCreatedAtBefore(LocalDate.of(2019, 9, 25).atStartOfDay().toInstant(ZoneOffset.UTC)); - assertThat(persons).hasSize(1).contains(person1); + .findAllByCreatedAtBefore(LocalDate.of(2019, 9, 25).atStartOfDay().toInstant(ZoneOffset.UTC)); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByIsNotNull(@Autowired PersonRepository repository) { List persons = repository.findAllByNullableIsNotNull(); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByIsNull(@Autowired PersonRepository repository) { List persons = repository.findAllByNullableIsNull(); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test void findByIn(@Autowired PersonRepository repository) { List persons = repository - .findAllByFirstNameIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); - assertThat(persons).hasSize(1).contains(person2); + .findAllByFirstNameIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test // GH-2301 void findByEmptyIn(@Autowired PersonRepository repository) { - List persons = repository - .findAllByFirstNameIn(Collections.emptyList()); + List persons = repository.findAllByFirstNameIn(Collections.emptyList()); assertThat(persons).isEmpty(); } @@ -3565,29 +4184,29 @@ class RepositoryIT { void findByNotIn(@Autowired PersonRepository repository) { List persons = repository - .findAllByFirstNameNotIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); - assertThat(persons).hasSize(1).contains(person1); + .findAllByFirstNameNotIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByEmpty(@Autowired PersonRepository repository) { List persons = repository.findAllByThingsIsEmpty(); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); } @Test void findByNotEmpty(@Autowired PersonRepository repository) { List persons = repository.findAllByThingsIsNotEmpty(); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test void findByExists(@Autowired PersonRepository repository) { List persons = repository.findAllByNullableExists(); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); } @Test @@ -3596,7 +4215,7 @@ class RepositoryIT { List persons; persons = repository.findAllByOrderByFirstNameAscBornOnDesc(); - assertThat(persons).containsExactly(person2, person1); + assertThat(persons).containsExactly(RepositoryIT.this.person2, RepositoryIT.this.person1); } @Test @@ -3605,43 +4224,46 @@ class RepositoryIT { List persons; persons = repository.findAllByPlaceNear(SFO); - assertThat(persons).containsExactly(person2, person1); + assertThat(persons).containsExactly(RepositoryIT.this.person2, RepositoryIT.this.person1); - persons = repository.findAllByPlaceNearAndFirstNameIn(SFO, Collections.singletonList(TEST_PERSON1_FIRST_NAME)); - assertThat(persons).containsExactly(person1); + persons = repository.findAllByPlaceNearAndFirstNameIn(SFO, + Collections.singletonList(TEST_PERSON1_FIRST_NAME)); + assertThat(persons).containsExactly(RepositoryIT.this.person1); Distance distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS); persons = repository.findAllByPlaceNear(MINC, distance); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByPlaceNear(CLARION, distance); assertThat(persons).isEmpty(); persons = repository.findAllByPlaceNear(MINC, Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); persons = repository.findAllByPlaceNear(MINC, Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); assertThat(persons).isEmpty(); - final Range distanceRange = Range.of(Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), - Bound.unbounded()); + final Range distanceRange = Range + .of(Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), Bound.unbounded()); persons = repository.findAllByPlaceNear(MINC, distanceRange); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); persons = repository.findAllByPlaceNear(distanceRange, MINC); - assertThat(persons).hasSize(1).contains(person2); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person2); persons = repository - .findAllByPlaceWithin(new Circle(new org.springframework.data.geo.Point(MINC.x(), MINC.y()), distance)); - assertThat(persons).hasSize(1).contains(person1); + .findAllByPlaceWithin(new Circle(new org.springframework.data.geo.Point(MINC.x(), MINC.y()), distance)); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); Box b = new Box( - new org.springframework.data.geo.Point(MINC.x() - distance.getValue(), MINC.y() - distance.getValue()), - new org.springframework.data.geo.Point(MINC.x() + distance.getValue(), MINC.y() + distance.getValue())); + new org.springframework.data.geo.Point(MINC.x() - distance.getValue(), + MINC.y() - distance.getValue()), + new org.springframework.data.geo.Point(MINC.x() + distance.getValue(), + MINC.y() + distance.getValue())); persons = repository.findAllByPlaceWithin(b); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); b = new Box(new org.springframework.data.geo.Point(NEO4J_HQ.x(), NEO4J_HQ.y()), new org.springframework.data.geo.Point(SFO.x(), SFO.y())); @@ -3659,10 +4281,11 @@ class RepositoryIT { new org.springframework.data.geo.Point(12.993747, 55.6122746)); persons = repository.findAllByPlaceWithin(BoundingBox.of(p)); - assertThat(persons).hasSize(1).contains(person1); + assertThat(persons).hasSize(1).contains(RepositoryIT.this.person1); - assertThatIllegalArgumentException().isThrownBy(() -> repository.findAllByPlaceWithin(p)).withMessage( - "The WITHIN operation does not support a class org.springframework.data.geo.Polygon, you might want to pass a bounding box instead: class org.springframework.data.neo4j.repository.query.BoundingBox.of(polygon)"); + assertThatIllegalArgumentException().isThrownBy(() -> repository.findAllByPlaceWithin(p)) + .withMessage( + "The WITHIN operation does not support a class org.springframework.data.geo.Polygon, you might want to pass a bounding box instead: class org.springframework.data.neo4j.repository.query.BoundingBox.of(polygon)"); persons = repository.findAllByPlaceNear(CLARION, distance); assertThat(persons).isEmpty(); @@ -3671,7 +4294,7 @@ class RepositoryIT { @Test void existsById(@Autowired PersonRepository repository) { - boolean exists = repository.existsById(id1); + boolean exists = repository.existsById(RepositoryIT.this.id1); assertThat(exists).isTrue(); } @@ -3693,8 +4316,9 @@ class RepositoryIT { void findBySomeCaseInsensitiveProperties(@Autowired PersonRepository repository) { List persons; - persons = repository.findAllByPlaceNearAndFirstNameAllIgnoreCase(SFO, TEST_PERSON1_FIRST_NAME.toUpperCase()); - assertThat(persons).containsExactly(person1); + persons = repository.findAllByPlaceNearAndFirstNameAllIgnoreCase(SFO, + TEST_PERSON1_FIRST_NAME.toUpperCase()); + assertThat(persons).containsExactly(RepositoryIT.this.person1); } @Test @@ -3703,8 +4327,9 @@ class RepositoryIT { List things; things = repository.findTop5ByOrderByNameDesc(); - assertThat(things).hasSize(5).extracting(ThingWithAssignedId::getName).containsExactlyInAnyOrder("name20", - "name19", "name18", "name17", "name16"); + assertThat(things).hasSize(5) + .extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("name20", "name19", "name18", "name17", "name16"); things = repository.findFirstByOrderByNameDesc(); assertThat(things).extracting(ThingWithAssignedId::getName).containsExactlyInAnyOrder("name20"); @@ -3721,6 +4346,7 @@ class RepositoryIT { long count = repository.countAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME); assertThat(count).isEqualTo(2L); } + } @Nested @@ -3728,14 +4354,20 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { - id1 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.nullable = 'something', n.first_name = $firstName RETURN id(n)", + RepositoryIT.this.id1 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.nullable = 'something', n.first_name = $firstName RETURN id(n)", Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME)) - .next().get(0).asLong(); - id2 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName RETURN id(n)", + .next() + .get(0) + .asLong(); + RepositoryIT.this.id2 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName RETURN id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME)) - .next().get(0).asLong(); + .next() + .get(0) + .asLong(); } @Test @@ -3749,24 +4381,22 @@ class RepositoryIT { @Test void mapsDtoProjectionWithDerivedFinderMethod(@Autowired PersonRepository repository) { - assertThat(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)) - .hasSize(1) - .extracting(DtoPersonProjection::getFirstName) - .first().isEqualTo(TEST_PERSON1_FIRST_NAME); + assertThat(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)).hasSize(1) + .extracting(DtoPersonProjection::getFirstName) + .first() + .isEqualTo(TEST_PERSON1_FIRST_NAME); } @Test // DATAGRAPH-1438 void mapsOptionalDtoProjectionWithDerivedFinderMethod(@Autowired PersonRepository repository) { - assertThat(repository.findOneByFirstName(TEST_PERSON1_FIRST_NAME)) - .map(DtoPersonProjection::getFirstName) - .hasValue(TEST_PERSON1_FIRST_NAME); - assertThat(repository.findOneByFirstName("foobar")) - .isEmpty(); + assertThat(repository.findOneByFirstName(TEST_PERSON1_FIRST_NAME)).map(DtoPersonProjection::getFirstName) + .hasValue(TEST_PERSON1_FIRST_NAME); + assertThat(repository.findOneByFirstName("foobar")).isEmpty(); assertThat(repository.findOneByNullable("something")).isNotNull() - .extracting(DtoPersonProjection::getFirstName) - .isEqualTo(TEST_PERSON1_FIRST_NAME); + .extracting(DtoPersonProjection::getFirstName) + .isEqualTo(TEST_PERSON1_FIRST_NAME); assertThat(repository.findOneByNullable("foobar")).isNull(); } @@ -3778,7 +4408,7 @@ class RepositoryIT { @Test void mapsInterfaceProjectionWithCustomQueryAndMapProjection(@Autowired PersonRepository repository) { assertThat(repository.findByNameWithCustomQueryAndMapProjection(TEST_PERSON1_NAME).getName()) - .isEqualTo(TEST_PERSON1_NAME); + .isEqualTo(TEST_PERSON1_NAME); } @Test @@ -3790,7 +4420,7 @@ class RepositoryIT { @Test void mapsInterfaceProjectionWithCustomQueryAndNodeReturn(@Autowired PersonRepository repository) { assertThat(repository.findByNameWithCustomQueryAndNodeReturn(TEST_PERSON1_NAME).getName()) - .isEqualTo(TEST_PERSON1_NAME); + .isEqualTo(TEST_PERSON1_NAME); } @Test @@ -3803,20 +4433,19 @@ class RepositoryIT { void mapDtoProjectionWithCustomQueryAndNodeReturn(@Autowired PersonRepository repository) { List projectedPeople = repository - .findAllDtoProjectionsWithAdditionalProperties(TEST_PERSON1_NAME); + .findAllDtoProjectionsWithAdditionalProperties(TEST_PERSON1_NAME); - assertThat(projectedPeople).hasSize(1) + assertThat(projectedPeople).hasSize(1).first().satisfies(dto -> { + assertThat(dto.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); + assertThat(dto.getSomeLongValue()).isEqualTo(4711L); + assertThat(dto.getSomeDoubles()).containsExactly(21.42, 42.21); + assertThat(dto.getOtherPeople()).hasSize(1) .first() - .satisfies(dto -> { - assertThat(dto.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); - assertThat(dto.getSomeLongValue()).isEqualTo(4711L); - assertThat(dto.getSomeDoubles()).containsExactly(21.42, 42.21); - assertThat(dto.getOtherPeople()).hasSize(1) - .first() - .extracting(PersonWithAllConstructor::getFirstName) - .isEqualTo(TEST_PERSON2_FIRST_NAME); - }); + .extracting(PersonWithAllConstructor::getFirstName) + .isEqualTo(TEST_PERSON2_FIRST_NAME); + }); } + } @Nested @@ -3825,14 +4454,15 @@ class RepositoryIT { @Override void setupData(TransactionContext transaction) { transaction.run( - "CREATE (:PersonWithAllConstructor{name: '%s', first_name: '%s'}), (:PersonWithAllConstructor{name: '%s'})".formatted(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON2_NAME) - ); + "CREATE (:PersonWithAllConstructor{name: '%s', first_name: '%s'}), (:PersonWithAllConstructor{name: '%s'})" + .formatted(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON2_NAME)); } @Test void streamMethodsShouldWork(@Autowired PersonRepository repository) { assertThat(repository.findAllByNameLike(TEST_PERSON1_NAME)).hasSize(2); } + } @Nested @@ -3877,7 +4507,8 @@ class RepositoryIT { @Test void findNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) { - Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); + Record record = doWithSession( + session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); long n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode()); long n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode()); long n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode()); @@ -3890,7 +4521,8 @@ class RepositoryIT { @Test void deleteNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) { - Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); + Record record = doWithSession( + session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); long n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode()); long n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode()); long n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode()); @@ -3918,8 +4550,10 @@ class RepositoryIT { } @Test - void createAllNodesWithMultipleLabels(@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) { - multipleLabelRepository.saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L))); + void createAllNodesWithMultipleLabels( + @Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) { + multipleLabelRepository + .saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L))); assertWithSession(session -> { Node node = session.run("MATCH (n:X) return n").single().get("n").asNode(); @@ -3949,10 +4583,8 @@ class RepositoryIT { void createNodeWithCustomIdAndDynamicLabels( @Autowired EntityWithCustomIdAndDynamicLabelsRepository repository) { - EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity1 - = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); - EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity2 - = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity1 = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity2 = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); entity1.identifier = "id1"; entity1.myLabels = Collections.singleton("LabelEntity1"); @@ -3968,10 +4600,9 @@ class RepositoryIT { assertWithSession(session -> { List result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity1) return e") - .list(); + .list(); assertThat(result).hasSize(1); - result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity2) return e") - .list(); + result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity2) return e").list(); assertThat(result).hasSize(1); }); } @@ -3979,8 +4610,9 @@ class RepositoryIT { @Test void findNodeWithMultipleLabels(@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) { - Record record = doWithSession(session -> session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") - .single()); + Record record = doWithSession(session -> session + .run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") + .single()); long n1Id = record.get("n1").asNode().get("id").asLong(); long n2Id = record.get("n2").asNode().get("id").asLong(); long n3Id = record.get("n3").asNode().get("id").asLong(); @@ -3993,7 +4625,9 @@ class RepositoryIT { @Test void deleteNodeWithMultipleLabels(@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) { - Record record = doWithSession(session -> session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3").single()); + Record record = doWithSession(session -> session + .run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") + .single()); long n1Id = record.get("n1").asNode().get("id").asLong(); long n2Id = record.get("n2").asNode().get("id").asLong(); long n3Id = record.get("n3").asNode().get("id").asLong(); @@ -4008,6 +4642,7 @@ class RepositoryIT { assertThat(session.run("MATCH (n:X) return n").list()).hasSize(1); }); } + } @Nested @@ -4041,35 +4676,38 @@ class RepositoryIT { baseClassRepository.save(ccB); assertThat(baseClassRepository.findByLabel("ConcreteClassA")).hasSize(1) - .first().isInstanceOf(Inheritance.ConcreteClassA.class) - .extracting(Inheritance.BaseClass::getName) - .isEqualTo("cc1"); + .first() + .isInstanceOf(Inheritance.ConcreteClassA.class) + .extracting(Inheritance.BaseClass::getName) + .isEqualTo("cc1"); assertThat(baseClassRepository.findByLabel("ConcreteClassB")).hasSize(1) - .first().isInstanceOf(Inheritance.ConcreteClassB.class) - .extracting(Inheritance.BaseClass::getName) - .isEqualTo("cc2"); + .first() + .isInstanceOf(Inheritance.ConcreteClassB.class) + .extracting(Inheritance.BaseClass::getName) + .isEqualTo("cc2"); List labels = new ArrayList<>(); labels.add("ConcreteClassA"); labels.add("ConcreteClassB"); assertThat(baseClassRepository.findByOrLabels(labels)).hasSize(2) - .hasOnlyElementsOfTypes(Inheritance.ConcreteClassA.class, Inheritance.ConcreteClassB.class) - .extracting(Inheritance.BaseClass::getName) - .containsExactlyInAnyOrder("cc1", "cc2"); + .hasOnlyElementsOfTypes(Inheritance.ConcreteClassA.class, Inheritance.ConcreteClassB.class) + .extracting(Inheritance.BaseClass::getName) + .containsExactlyInAnyOrder("cc1", "cc2"); assertThat(baseClassRepository.findByAndLabels(labels)).hasSize(0); String labelsString = "ConcreteClassA"; assertThat(baseClassRepository.findByAndLabels(labelsString)).hasSize(1) - .first().isInstanceOf(Inheritance.ConcreteClassA.class) - .extracting(Inheritance.BaseClass::getName) - .isEqualTo("cc1"); + .first() + .isInstanceOf(Inheritance.ConcreteClassA.class) + .extracting(Inheritance.BaseClass::getName) + .isEqualTo("cc1"); assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> baseClassRepository.findByAndLabels(1)) - .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) - .withMessageContaining("Cannot process argument"); + .havingRootCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Cannot process argument"); } @@ -4089,8 +4727,10 @@ class RepositoryIT { void findAllWithInheritanceAndExplicitLabeling(@Autowired BaseClassWithLabelsRepository repository) { String classAName = "test1"; String classBName = "test2"; - Inheritance.ExtendingClassWithLabelsA classWithLabelsA = new Inheritance.ExtendingClassWithLabelsA(classAName); - Inheritance.ExtendingClassWithLabelsB classWithLabelsB = new Inheritance.ExtendingClassWithLabelsB(classBName); + Inheritance.ExtendingClassWithLabelsA classWithLabelsA = new Inheritance.ExtendingClassWithLabelsA( + classAName); + Inheritance.ExtendingClassWithLabelsB classWithLabelsB = new Inheritance.ExtendingClassWithLabelsB( + classBName); repository.save(classWithLabelsA); repository.save(classWithLabelsB); @@ -4121,8 +4761,8 @@ class RepositoryIT { Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA(concreteClassName, someValue); ccA.others = Collections.singletonList(new Inheritance.ConcreteClassB("ccB", 41)); neo4jTemplate.save(ccA); - List ccAs = neo4jTemplate.findAll("MATCH (a:SuperBaseClass{name: 'cc1'})-[r]->(m) " + - "RETURN a, collect(r), collect(m)", + List ccAs = neo4jTemplate.findAll( + "MATCH (a:SuperBaseClass{name: 'cc1'})-[r]->(m) " + "RETURN a, collect(r), collect(m)", Inheritance.SuperBaseClass.class); assertThat(ccAs).hasSize(1); Inheritance.SuperBaseClass loadedCcA = ccAs.get(0); @@ -4209,8 +4849,7 @@ class RepositoryIT { List things = new ArrayList<>(); things.add(ccA); things.add(ccB); - Inheritance.ExtendingBaseClassWithRelationship thing - = new Inheritance.ExtendingBaseClassWithRelationship(); + Inheritance.ExtendingBaseClassWithRelationship thing = new Inheritance.ExtendingBaseClassWithRelationship(); thing.setThings(things); Inheritance.ConcreteClassA ccC = new Inheritance.ConcreteClassA("cc3", "A"); @@ -4222,7 +4861,7 @@ class RepositoryIT { assertThat(all.get(0).getThings()).containsExactlyInAnyOrder(ccA, ccB); assertThat(((Inheritance.ExtendingBaseClassWithRelationship) all.get(0)).getSomethingConcrete()) - .containsExactlyInAnyOrder(ccC); + .containsExactlyInAnyOrder(ccC); } @Test // DATAGRAPH-1467 @@ -4236,8 +4875,7 @@ class RepositoryIT { List things = new ArrayList<>(); things.add(ccA); things.add(ccB1); - Inheritance.ExtendingBaseClassWithRelationship thing - = new Inheritance.ExtendingBaseClassWithRelationship(); + Inheritance.ExtendingBaseClassWithRelationship thing = new Inheritance.ExtendingBaseClassWithRelationship(); thing.setThings(things); Inheritance.ConcreteClassA ccC = new Inheritance.ConcreteClassA("cc3", "A"); @@ -4248,14 +4886,13 @@ class RepositoryIT { List all = repository.findAll(); - assertThat(all.get(0).getBoing()) - .containsExactlyInAnyOrder(ccB2); + assertThat(all.get(0).getBoing()).containsExactlyInAnyOrder(ccB2); assertThat(((Inheritance.ExtendingBaseClassWithRelationship) all.get(0)).getThings()) - .containsExactlyInAnyOrder(ccA, ccB1); + .containsExactlyInAnyOrder(ccA, ccB1); assertThat(((Inheritance.ExtendingBaseClassWithRelationship) all.get(0)).getSomethingConcrete()) - .containsExactlyInAnyOrder(ccC); + .containsExactlyInAnyOrder(ccC); } @Test // DATAGRAPH-1467 @@ -4267,22 +4904,20 @@ class RepositoryIT { List things = new ArrayList<>(); - Inheritance.SuperBaseClassRelationshipProperties relCcA = - new Inheritance.SuperBaseClassRelationshipProperties(ccA); + Inheritance.SuperBaseClassRelationshipProperties relCcA = new Inheritance.SuperBaseClassRelationshipProperties( + ccA); - Inheritance.SuperBaseClassRelationshipProperties relCcB - = new Inheritance.SuperBaseClassRelationshipProperties(ccB); + Inheritance.SuperBaseClassRelationshipProperties relCcB = new Inheritance.SuperBaseClassRelationshipProperties( + ccB); things.add(relCcA); things.add(relCcB); - Inheritance.ExtendingBaseClassWithRelationshipProperties thing - = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); + Inheritance.ExtendingBaseClassWithRelationshipProperties thing = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); thing.setThings(things); Inheritance.ConcreteClassA ccC = new Inheritance.ConcreteClassA("cc3", "A"); - Inheritance.ConcreteARelationshipProperties relCcc = - new Inheritance.ConcreteARelationshipProperties(ccC); + Inheritance.ConcreteARelationshipProperties relCcc = new Inheritance.ConcreteARelationshipProperties(ccC); thing.setSomethingConcrete(Collections.singletonList(relCcc)); @@ -4292,7 +4927,7 @@ class RepositoryIT { assertThat(all.get(0).getThings()).containsExactlyInAnyOrder(relCcA, relCcB); assertThat(((Inheritance.ExtendingBaseClassWithRelationshipProperties) all.get(0)).getSomethingConcrete()) - .containsExactlyInAnyOrder(relCcc); + .containsExactlyInAnyOrder(relCcc); } @Test // DATAGRAPH-1467 @@ -4305,25 +4940,22 @@ class RepositoryIT { List things = new ArrayList<>(); - Inheritance.SuperBaseClassRelationshipProperties relCcA = - new Inheritance.SuperBaseClassRelationshipProperties(ccA); + Inheritance.SuperBaseClassRelationshipProperties relCcA = new Inheritance.SuperBaseClassRelationshipProperties( + ccA); - Inheritance.SuperBaseClassRelationshipProperties relCcB1 - = new Inheritance.SuperBaseClassRelationshipProperties(ccB1); + Inheritance.SuperBaseClassRelationshipProperties relCcB1 = new Inheritance.SuperBaseClassRelationshipProperties( + ccB1); - Inheritance.ConcreteBRelationshipProperties relCcB2 - = new Inheritance.ConcreteBRelationshipProperties(ccB2); + Inheritance.ConcreteBRelationshipProperties relCcB2 = new Inheritance.ConcreteBRelationshipProperties(ccB2); things.add(relCcA); things.add(relCcB1); - Inheritance.ExtendingBaseClassWithRelationshipProperties thing - = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); + Inheritance.ExtendingBaseClassWithRelationshipProperties thing = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); thing.setThings(things); Inheritance.ConcreteClassA ccC = new Inheritance.ConcreteClassA("cc3", "A"); - Inheritance.ConcreteARelationshipProperties relCcc = - new Inheritance.ConcreteARelationshipProperties(ccC); + Inheritance.ConcreteARelationshipProperties relCcc = new Inheritance.ConcreteARelationshipProperties(ccC); thing.setSomethingConcrete(Collections.singletonList(relCcc)); thing.setBoing(Collections.singletonList(relCcB2)); @@ -4332,14 +4964,13 @@ class RepositoryIT { List all = repository.findAll(); - assertThat(all.get(0).getBoing()) - .containsExactlyInAnyOrder(relCcB2); + assertThat(all.get(0).getBoing()).containsExactlyInAnyOrder(relCcB2); assertThat(((Inheritance.ExtendingBaseClassWithRelationshipProperties) all.get(0)).getThings()) - .containsExactlyInAnyOrder(relCcA, relCcB1); + .containsExactlyInAnyOrder(relCcA, relCcB1); assertThat(((Inheritance.ExtendingBaseClassWithRelationshipProperties) all.get(0)).getSomethingConcrete()) - .containsExactlyInAnyOrder(relCcc); + .containsExactlyInAnyOrder(relCcc); } @Test // DATAGRAPH-1467 @@ -4352,25 +4983,22 @@ class RepositoryIT { List things = new ArrayList<>(); - Inheritance.SuperBaseClassRelationshipProperties relCcA = - new Inheritance.SuperBaseClassRelationshipProperties(ccA); + Inheritance.SuperBaseClassRelationshipProperties relCcA = new Inheritance.SuperBaseClassRelationshipProperties( + ccA); - Inheritance.SuperBaseClassRelationshipProperties relCcB1 - = new Inheritance.SuperBaseClassRelationshipProperties(ccB1); + Inheritance.SuperBaseClassRelationshipProperties relCcB1 = new Inheritance.SuperBaseClassRelationshipProperties( + ccB1); - Inheritance.ConcreteBRelationshipProperties relCcB2 - = new Inheritance.ConcreteBRelationshipProperties(ccB2); + Inheritance.ConcreteBRelationshipProperties relCcB2 = new Inheritance.ConcreteBRelationshipProperties(ccB2); things.add(relCcA); things.add(relCcB1); - Inheritance.ExtendingBaseClassWithRelationshipProperties thing - = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); + Inheritance.ExtendingBaseClassWithRelationshipProperties thing = new Inheritance.ExtendingBaseClassWithRelationshipProperties(); thing.setThings(things); Inheritance.ConcreteClassA ccC = new Inheritance.ConcreteClassA("cc3", "A"); - Inheritance.ConcreteARelationshipProperties relCcc = - new Inheritance.ConcreteARelationshipProperties(ccC); + Inheritance.ConcreteARelationshipProperties relCcc = new Inheritance.ConcreteARelationshipProperties(ccC); thing.setSomethingConcrete(Collections.singletonList(relCcc)); thing.setBoing(Collections.singletonList(relCcB2)); @@ -4380,8 +5008,9 @@ class RepositoryIT { List all = repository.getAllWithHasRelationships(); assertThat(((Inheritance.ExtendingBaseClassWithRelationshipProperties) all.get(0)).getThings()) - .containsExactlyInAnyOrder(relCcA, relCcB1); + .containsExactlyInAnyOrder(relCcA, relCcB1); } + } @Nested @@ -4389,15 +5018,19 @@ class RepositoryIT { @Test void findByPropertyOnRelatedEntity(@Autowired RelationshipRepository repository) { - doWithSession(session -> session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})").consume()); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})") + .consume()); assertThat(repository.findByPetsName("Jerry").getName()).isEqualTo("Freddie"); } @Test void findByPropertyOnRelatedEntitiesOr(@Autowired RelationshipRepository repository) { - doWithSession(session -> session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," - + "(n)-[:Has]->(:Hobby{name: 'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + + "(n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); assertThat(repository.findByHobbiesNameOrPetsName("Music", "Jerry").getName()).isEqualTo("Freddie"); assertThat(repository.findByHobbiesNameOrPetsName("Sports", "Tom").getName()).isEqualTo("Freddie"); @@ -4406,9 +5039,10 @@ class RepositoryIT { @Test void findByPropertyOnRelatedEntitiesAnd(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," - + "(n)-[:Has]->(:Hobby{name: 'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + + "(n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); assertThat(repository.findByHobbiesNameAndPetsName("Music", "Tom").getName()).isEqualTo("Freddie"); assertThat(repository.findByHobbiesNameAndPetsName("Sports", "Jerry")).isNull(); @@ -4416,9 +5050,10 @@ class RepositoryIT { @Test void findByPropertyOnRelatedEntityOfRelatedEntity(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" - + "-[:Has]->(:Hobby{name: 'Sleeping'})").consume()); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + + "-[:Has]->(:Hobby{name: 'Sleeping'})") + .consume()); assertThat(repository.findByPetsHobbiesName("Sleeping").getName()).isEqualTo("Freddie"); assertThat(repository.findByPetsHobbiesName("Sports")).isNull(); @@ -4426,9 +5061,10 @@ class RepositoryIT { @Test void findByPropertyOnRelatedEntityOfRelatedSameEntity(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" - + "-[:Has]->(:Pet{name: 'Tom'})").consume()); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + + "-[:Has]->(:Pet{name: 'Tom'})") + .consume()); assertThat(repository.findByPetsFriendsName("Tom").getName()).isEqualTo("Freddie"); assertThat(repository.findByPetsFriendsName("Jerry")).isNull(); @@ -4436,28 +5072,31 @@ class RepositoryIT { @Test // GH-2243 void findDistinctByRelatedEntity(@Autowired RelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Hobby{name: 'Music'})" - + "CREATE (n)-[:Has]->(:Hobby{name: 'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Hobby{name: 'Music'})" + + "CREATE (n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); assertThat(repository.findDistinctByHobbiesName("Music")).isNotNull(); } @Test - void findByPropertyOnRelationshipWithProperties(@Autowired PersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})").consume()); + void findByPropertyOnRelationshipWithProperties( + @Autowired PersonWithRelationshipWithPropertiesRepository repository) { + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})") + .consume()); assertThat(repository.findByHobbiesSince(2020).getName()).isEqualTo("Freddie"); } @Test - void findByPropertyOnRelationshipWithPropertiesOr(@Autowired PersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume()); + void findByPropertyOnRelationshipWithPropertiesOr( + @Autowired PersonWithRelationshipWithPropertiesRepository repository) { + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); assertThat(repository.findByHobbiesSinceOrHobbiesActive(2020, false).getName()).isEqualTo("Freddie"); assertThat(repository.findByHobbiesSinceOrHobbiesActive(2019, true).getName()).isEqualTo("Freddie"); @@ -4465,10 +5104,11 @@ class RepositoryIT { } @Test - void findByPropertyOnRelationshipWithPropertiesAnd(@Autowired PersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume()); + void findByPropertyOnRelationshipWithPropertiesAnd( + @Autowired PersonWithRelationshipWithPropertiesRepository repository) { + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); assertThat(repository.findByHobbiesSinceAndHobbiesActive(2020, true).getName()).isEqualTo("Freddie"); assertThat(repository.findByHobbiesSinceAndHobbiesActive(2019, true)).isNull(); @@ -4478,9 +5118,9 @@ class RepositoryIT { @Test void findByPropertyOnRelationshipWithPropertiesRelatedEntity( @Autowired PersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume()); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); assertThat(repository.findByHobbiesHobbyName("Bowling").getName()).isEqualTo("Freddie"); } @@ -4488,46 +5128,28 @@ class RepositoryIT { @Test void findByCustomQueryOnlyWithPropertyReturn( @Autowired PersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume()); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); assertThat(repository.justTheNames().getName()).isEqualTo("Freddie"); } - } - @Test // GH-2706 - void findByOffsetDateTimeShouldWork(@Autowired TemporalRepository temporalRepository) { - - temporalRepository.deleteAll(); - - LocalDateTime fixedDateTime = LocalDateTime.of(2023, 1, 1, 21, 21, 0); - ZoneId europeBerlin = TimeZone.getTimeZone("Europe/Berlin").toZoneId(); - OffsetDateTime v1 = OffsetDateTime.of(fixedDateTime, europeBerlin.getRules().getOffset(fixedDateTime)); - LocalTime v2 = fixedDateTime.toLocalTime(); - - temporalRepository.save(new OffsetTemporalEntity(v1, v2)); - temporalRepository.save(new OffsetTemporalEntity(v1.minusDays(2), v2.minusMinutes(2))); - - assertThat(temporalRepository.findAllByProperty1After(v1)).isEmpty(); - assertThat(temporalRepository.findAllByProperty2After(v2)).isEmpty(); - - assertThat(temporalRepository.findAllByProperty1After(v1.minusDays(1))).hasSize(1); - assertThat(temporalRepository.findAllByProperty2After(v2.minusMinutes(1))).hasSize(1); } /** - * The tests in this class ensure that in case of an inheritance scenario no DTO is projected but the extending class - * is used. If it wasn't the case, we wouldn't find the relationship nor the other attribute. + * The tests in this class ensure that in case of an inheritance scenario no DTO is + * projected but the extending class is used. If it wasn't the case, we wouldn't find + * the relationship nor the other attribute. */ @Nested class DtoVsInheritance extends IntegrationTestBase { @Override void setupData(TransactionContext transaction) { - transaction.run("" - + "create (p:ParentNode:ExtendedParentNode {someAttribute: 'Foo', someOtherAttribute: 'Bar'})" - + "create (p) -[:CONNECTED_TO]-> (:PersonWithAllConstructor {name: 'Bazbar'})"); + transaction + .run("" + "create (p:ParentNode:ExtendedParentNode {someAttribute: 'Foo', someOtherAttribute: 'Bar'})" + + "create (p) -[:CONNECTED_TO]-> (:PersonWithAllConstructor {name: 'Bazbar'})"); } @Test @@ -4547,424 +5169,7 @@ class RepositoryIT { assertThat(ep.getPeople()).extracting(PersonWithAllConstructor::getName).containsExactly("Bazbar"); }); } - } - - interface BidirectionalExternallyGeneratedIdRepository - extends Neo4jRepository {} - - interface BidirectionalAssignedIdRepository - extends Neo4jRepository {} - - interface BidirectionalStartRepository extends Neo4jRepository {} - - interface BidirectionalEndRepository extends Neo4jRepository {} - - interface LoopingRelationshipRepository extends Neo4jRepository {} - - interface ImmutablePersonRepository extends Neo4jRepository {} - - interface MultipleLabelRepository extends Neo4jRepository {} - - interface MultipleLabelWithAssignedIdRepository - extends Neo4jRepository {} - - interface PersonWithRelationshipWithPropertiesRepository - extends Neo4jRepository { - - @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") - PersonWithRelationshipWithProperties loadFromCustomQuery(@Param("id") Long id); - - PersonWithRelationshipWithProperties findByHobbiesSince(int since); - - PersonWithRelationshipWithProperties findByHobbiesSinceOrHobbiesActive(int since1, boolean active); - - PersonWithRelationshipWithProperties findByHobbiesSinceAndHobbiesActive(int since1, boolean active); - - PersonWithRelationshipWithProperties findByHobbiesHobbyName(String hobbyName); - - @Query("MATCH (p:PersonWithRelationshipWithProperties) return p {.name}") - PersonWithRelationshipWithProperties justTheNames(); - } - - interface PetRepository extends Neo4jRepository { - - @Query("MATCH (p:Pet)-[r1:Has]->(p2:Pet)-[r2:Has]->(p3:Pet) " + - "where id(p) = $petNode1Id return p, collect(r1), collect(p2), collect(r2), collect(p3)") - Pet customQueryWithDeepRelationshipMapping(@Param("petNode1Id") long petNode1Id); - @Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)") - Page pagedPets(Pageable pageable); - - @Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)") - Slice slicedPets(Pageable pageable); - - @Query(value = "MATCH (p:#{#staticLabels}) where p.name=$petName return p SKIP $skip LIMIT $limit", - countQuery = "MATCH (p:#{#staticLabels}) return count(p)") - Page pagedPetsWithParameter(@Param("petName") String petName, Pageable pageable); - - Pet findByFriendsName(String friendName); - - Long deleteByNameAndFriendsName(String name, String friendsName); - - Pet findByFriendsFriendsName(String friendName); - - long countByName(String name); - - @Query(value = "RETURN size($0)", count = true) - long countAllByName(String name); - - long countByFriendsNameAndFriendsFriendsName(String friendName, String friendFriendName); - - boolean existsByName(String name); - - @Query("MATCH (n:Pet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:Pet) return n, collect(r), collect(m)") - List findLunas(); - - @Query("MATCH (p:Pet)" - + " OPTIONAL MATCH (p)-[rel:Has]->(op)" - + " RETURN p, collect(rel), collect(op)") - List findAllFriends(); - } - - interface ImmutablePetRepository extends Neo4jRepository { - - @Query("MATCH (n:ImmutablePet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:ImmutablePet) return n, collect(r), collect(m)") - List findLunas(); } - interface OneToOneRepository extends Neo4jRepository { - - @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, collect(r), collect(p2)") - List findAllWithCustomQuery(); - - @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, r, p2") - List findAllWithCustomQueryNoCollect(); - - @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) WHERE p1.name = $0 return p1, r, p2") - Optional findOneByName(String name); - - @Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return *") - List findAllWithCustomQueryReturnStar(); - - @Query("MATCH (p1:#{#staticLabels}) OPTIONAL MATCH (p1)-[r:OWNS]->(p2:OneToOneTarget) return p1, r, p2") - List findAllWithNullValues(); - } - - interface RelationshipRepository extends Neo4jRepository { - - @Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) " - + "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets " - + "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) " - + "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies") - PersonWithRelationship getPersonWithRelationshipsViaQuery(); - - @Query("MATCH p=(n:PersonWithRelationship{name:'Freddie'})-[:Has*]->(something) " - + "return n, collect(relationships(p)), collect(nodes(p))") - PersonWithRelationship getPersonWithRelationshipsViaPathQuery(); - - PersonWithRelationship findByPetsName(String petName); - - PersonWithRelationship findByName(String name); - - Page findByName(String name, Pageable pageable); - - List findByName(String name, Sort sort); - - PersonWithRelationship findByHobbiesNameOrPetsName(String hobbyName, String petName); - - PersonWithRelationship findByHobbiesNameAndPetsName(String hobbyName, String petName); - - PersonWithRelationship findByPetsHobbiesName(String hobbyName); - - PersonWithRelationship findByPetsFriendsName(String petName); - - @Transactional - @Query("CREATE (n:PersonWithRelationship) \n" - + "SET n.name = $0.__properties__.name \n" - + "WITH n, id(n) as parentId\n" - + "UNWIND $0.__properties__.Has as x\n" - + "CALL { WITH x, parentId\n" - + " \n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Pet'\n" - + " CREATE (p:Pet {name: x.__properties__.name}) <- [r:Has] - (_)\n" - + " RETURN p, r\n" - + " \n" - + " UNION\n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Hobby'\n" - + " CREATE (p:Hobby {name: x.__properties__.name}) <- [r:Has] - (_)\n" - + " RETURN p, r\n" - + "\n" - + " UNION\n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Club'\n" - + " CREATE (p:Club {name: x.__properties__.name}) - [r:Has] -> (_)\n" - + " RETURN p, r\n" - + "\n" - + "}\n" - + "RETURN n, collect(r), collect(p)") - PersonWithRelationship createWithCustomQuery(PersonWithRelationship p); - - @Transactional - @Query("UNWIND $0 AS pwr WITH pwr CREATE (n:PersonWithRelationship) \n" - + "SET n.name = pwr.__properties__.name \n" - + "WITH pwr, n, id(n) as parentId\n" - + "UNWIND pwr.__properties__.Has as x\n" - + "CALL { WITH x, parentId\n" - + " \n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Pet'\n" - + " CREATE (p:Pet {name: x.__properties__.name}) <- [r:Has] - (_)\n" - + " RETURN p, r\n" - + " \n" - + " UNION\n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Hobby'\n" - + " CREATE (p:Hobby {name: x.__properties__.name}) <- [r:Has] - (_)\n" - + " RETURN p, r\n" - + "\n" - + " UNION\n" - + " WITH x, parentId\n" - + " MATCH (_) \n" - + " WHERE id(_) = parentId AND x.__labels__[0] = 'Club'\n" - + " CREATE (p:Club {name: x.__properties__.name}) - [r:Has] -> (_)\n" - + " RETURN p, r\n" - + "\n" - + "}\n" - + "RETURN n, collect(r), collect(p)") - List createManyWithCustomQuery(Collection p); - - PersonWithRelationship.PersonWithHobby findDistinctByHobbiesName(String hobbyName); - } - - interface SimilarThingRepository extends Neo4jRepository {} - - interface BaseClassRepository extends Neo4jRepository { - - @Query("MATCH (n::#{literal(#label)}) RETURN n") - List findByLabel(@Param("label") String label); - - @Query("MATCH (n::#{anyOf(#label)}) RETURN n") - List findByOrLabels(@Param("label") List labels); - - @Query("MATCH (n::#{allOf(#label)}) RETURN n") - List findByAndLabels(@Param("label") Object labels); - } - - interface SuperBaseClassRepository extends Neo4jRepository { - - @Query("MATCH (n:SuperBaseClass) return n") - List getAllConcreteTypes(); - } - - interface RelationshipToAbstractClassRepository - extends Neo4jRepository { - - @Query("MATCH (n:RelationshipToAbstractClass)-[h:HAS]->(m:SuperBaseClass) return n, collect(h), collect(m)") - Inheritance.RelationshipToAbstractClass getAllConcreteRelationships(); - } - - interface BaseClassWithRelationshipRepository - extends Neo4jRepository {} - - interface SuperBaseClassWithRelationshipRepository - extends Neo4jRepository {} - - interface BaseClassWithRelationshipPropertiesRepository - extends Neo4jRepository {} - - interface SuperBaseClassWithRelationshipPropertiesRepository - extends Neo4jRepository { - - @Query("MATCH (n:SuperBaseClassWithRelationshipProperties)" + - "-[h:HAS]->" + - "(m:SuperBaseClass) return n, collect(h), collect(m)") - List getAllWithHasRelationships(); - - } - - interface BaseClassWithLabelsRepository extends Neo4jRepository {} - - interface EntityWithConvertedIdRepository - extends Neo4jRepository {} - - interface HobbyWithRelationshipWithPropertiesRepository extends Neo4jRepository { - - @Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)") - AltHobby loadFromCustomQuery(@Param("personId") Long personId); - } - - interface FriendRepository extends Neo4jRepository {} - - interface KotlinPersonRepository extends Neo4jRepository { - - @Query("MATCH (n:KotlinPerson)-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") - List getAllKotlinPersonsViaQuery(); - - @Query("MATCH (n:KotlinPerson{name:'Test'})-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") - KotlinPerson getOneKotlinPersonViaQuery(); - - @Query("MATCH (n:KotlinPerson{name:'Test'})-[w:WORKS_IN]->(c:KotlinClub) return n, collect(w), collect(c)") - Optional getOptionalKotlinPersonViaQuery(); - } - - interface ParentRepository extends Neo4jRepository { - - /** - * Ensure things can be found by base attribute. - * @param someAttribute Base attribute - * @return optional entity - */ - Optional findExtendedParentNodeBySomeAttribute(String someAttribute); - - /** - * Ensure things can be found by extended attribute. - * @param someOtherAttribute Base attribute - * @return optional entity - */ - Optional findExtendedParentNodeBySomeOtherAttribute(String someOtherAttribute); - } - - interface SimpleEntityWithRelationshipARepository extends Neo4jRepository {} - - interface ThingWithFixedGeneratedIdRepository extends Neo4jRepository {} - - interface EntityWithRelationshipPropertiesPathRepository - extends Neo4jRepository {} - - interface BidirectionalSameEntityRepository extends Neo4jRepository {} - - interface SameIdEntitiesWithRelationshipPropertiesRepository - extends Neo4jRepository {} - - interface SameIdEntitiesRepository extends Neo4jRepository {} - - interface EntityWithCustomIdAndDynamicLabelsRepository - extends Neo4jRepository {} - - interface TemporalRepository extends - Neo4jRepository { - - List findAllByProperty1After(OffsetDateTime aValue); - - List findAllByProperty2After(LocalTime aValue); - - } - - @SpringJUnitConfig(Config.class) - static abstract class IntegrationTestBase { - - @Autowired private Driver driver; - - @Autowired private TransactionTemplate transactionalOperator; - - @Autowired private BookmarkCapture bookmarkCapture; - - void setupData(TransactionContext transaction) { - } - - @BeforeEach - void before() { - doWithSession(session -> - session.executeWrite(tx -> { - tx.run("MATCH (n) detach delete n").consume(); - setupData(tx); - return null; - })); - } - - T doWithSession(Function sessionConsumer) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { - T result = sessionConsumer.apply(session); - bookmarkCapture.seedWith(session.lastBookmarks()); - return result; - } - } - - void assertWithSession(Consumer consumer) { - - try (Session session = driver.session(bookmarkCapture.createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { - consumer.accept(session); - } - } - } - - @Configuration - @EnableNeo4jRepositories(considerNestedRepositories = true) - @EnableTransactionManagement - static class Config extends Neo4jImperativeTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Override - protected Collection getMappingBasePackages() { - return Arrays.asList( - PersonWithAllConstructor.class.getPackage().getName(), - Flight.class.getPackage().getName() - ); - } - - @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { - - Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); - mappingContext.setInitialEntitySet(getInitialEntitySet()); - mappingContext.setStrict(true); - - return mappingContext; - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { - - return Neo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .withBookmarkManager(Neo4jBookmarkManager.create(bookmarkCapture())) - .build(); - } - - @Override - public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) { - - return Neo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .build(); - } - - @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { - return new TransactionTemplate(transactionManager); - } - - @Override - public DatabaseSelectionProvider databaseSelectionProvider() { - return () -> databaseSelection.get(); - } - - @Bean - public UserSelectionProvider getUserSelectionProvider() { - return () -> userSelection.get(); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java index dcc5d85f2..cd03ce497 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.test.Neo4jExtension; @@ -54,4 +55,5 @@ class RepositoryWithADifferentDatabaseIT extends RepositoryIT { databaseSelection.set(DatabaseSelection.undecided()); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentUserIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentUserIT.java index c003c75b9..1e7a0e349 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentUserIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentUserIT.java @@ -15,18 +15,19 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assumptions.assumeThat; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -36,6 +37,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; class RepositoryWithADifferentUserIT extends RepositoryIT { private static final String TEST_USER = "sdn62"; + private static final String TEST_DATABASE_NAME = "sdn62db"; @BeforeAll @@ -46,12 +48,13 @@ class RepositoryWithADifferentUserIT extends RepositoryIT { try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) { session.run("CREATE DATABASE $db", Values.parameters("db", TEST_DATABASE_NAME)).consume(); - session.run("CREATE USER $user SET PASSWORD $password CHANGE NOT REQUIRED SET HOME DATABASE $database", - Values.parameters("user", TEST_USER, "password", TEST_USER + "_password", "database", TEST_DATABASE_NAME)) - .consume(); + session + .run("CREATE USER $user SET PASSWORD $password CHANGE NOT REQUIRED SET HOME DATABASE $database", Values + .parameters("user", TEST_USER, "password", TEST_USER + "_password", "database", TEST_DATABASE_NAME)) + .consume(); session.run("GRANT ROLE publisher TO $user", Values.parameters("user", TEST_USER)).consume(); session.run("GRANT IMPERSONATE ($targetUser) ON DBMS TO admin", Values.parameters("targetUser", TEST_USER)) - .consume(); + .consume(); } userSelection.set(UserSelection.impersonate(TEST_USER)); @@ -62,12 +65,14 @@ class RepositoryWithADifferentUserIT extends RepositoryIT { try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) { - session.run("REVOKE IMPERSONATE ($targetUser) ON DBMS FROM admin", - Values.parameters("targetUser", TEST_USER)).consume(); + session + .run("REVOKE IMPERSONATE ($targetUser) ON DBMS FROM admin", Values.parameters("targetUser", TEST_USER)) + .consume(); session.run("DROP USER $user", Values.parameters("user", TEST_USER)).consume(); session.run("DROP DATABASE $db", Values.parameters("db", TEST_DATABASE_NAME)).consume(); } userSelection.set(UserSelection.connectedUser()); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java index fd2df3792..988773663 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Map; import java.util.function.Function; @@ -29,6 +27,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,6 +51,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -60,6 +61,39 @@ class ScrollingIT { @SuppressWarnings("unused") private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + @Configuration + @EnableNeo4jRepositories + @EnableTransactionManagement + static class Config extends Neo4jImperativeTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + @Nested @SpringJUnitConfig(Config.class) @DisplayName("Scroll with derived finder method") @@ -67,10 +101,8 @@ class ScrollingIT { @BeforeAll static void setupTestData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { - try ( - var session = driver.session(bookmarkCapture.createSessionConfig()); - var transaction = session.beginTransaction() - ) { + try (var session = driver.session(bookmarkCapture.createSessionConfig()); + var transaction = session.beginTransaction()) { ScrollingEntity.createTestData(transaction); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -81,10 +113,7 @@ class ScrollingIT { void oneColumnSortNoScroll(@Autowired ScrollingRepository repository) { var topN = repository.findTop4ByOrderByB(); - assertThat(topN) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0", "C0", "D0"); + assertThat(topN).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("A0", "B0", "C0", "D0"); } @Test @@ -95,33 +124,30 @@ class ScrollingIT { var window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, ScrollPosition.keyset()); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(0).getId()), Index.atIndex(3)) - .extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0", "C0", "D0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(0).getId()), Index.atIndex(3)) + .extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0", "C0", "D0"); window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, window.positionAt(window.size() - 1)); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(1).getId()), Index.atIndex(0)) - .extracting(ScrollingEntity::getA) - .containsExactly("D0", "E0", "F0", "G0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(1).getId()), Index.atIndex(0)) + .extracting(ScrollingEntity::getA) + .containsExactly("D0", "E0", "F0", "G0"); window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, window.positionAt(window.size() - 1)); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("H0", "I0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("H0", "I0"); } @Test void forwardWithDuplicatesIteratorIteration(@Autowired ScrollingRepository repository) { var it = WindowIterator.of(pos -> repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, pos)) - .startingAt(ScrollPosition.keyset()); + .startingAt(ScrollPosition.keyset()); var content = new ArrayList(); while (it.hasNext()) { var next = it.next(); @@ -129,8 +155,7 @@ class ScrollingIT { } assertThat(content).hasSize(10); - assertThat(content.stream().map(ScrollingEntity::getId) - .distinct().toList()).hasSize(10); + assertThat(content.stream().map(ScrollingEntity::getId).distinct().toList()).hasSize(10); } @Test @@ -138,39 +163,32 @@ class ScrollingIT { // Recreate the last position var last = repository.findFirstByA("I0"); - var keys = Map.of( - "foobar", Values.value(last.getA()), - "b", Values.value(last.getB()), - Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) - ); + var keys = Map.of("foobar", Values.value(last.getA()), "b", Values.value(last.getB()), + Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString())); var duplicates = repository.findAllByAOrderById("D0"); assertThat(duplicates).hasSize(2); var window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, ScrollPosition.backward(keys)); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("F0", "G0", "H0", "I0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("F0", "G0", "H0", "I0"); var pos = ((KeysetScrollPosition) window.positionAt(0)); pos = ScrollPosition.backward(pos.getKeys()); window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, pos); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .extracting(ScrollingEntity::getA) - .containsExactly("C0", "D0", "D0", "E0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("C0", "D0", "D0", "E0"); pos = ((KeysetScrollPosition) window.positionAt(0)); pos = ScrollPosition.backward(pos.getKeys()); window = repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, pos); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("A0", "B0"); } + } @Nested @@ -180,10 +198,8 @@ class ScrollingIT { @BeforeAll static void setupTestData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { - try ( - var session = driver.session(bookmarkCapture.createSessionConfig()); - var transaction = session.beginTransaction() - ) { + try (var session = driver.session(bookmarkCapture.createSessionConfig()); + var transaction = session.beginTransaction()) { ScrollingEntity.createTestDataWithoutDuplicates(transaction); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -194,91 +210,56 @@ class ScrollingIT { @Tag("GH-2726") void forwardWithFluentQueryByExample(@Autowired ScrollingRepository scrollingRepository) { ScrollingEntity scrollingEntity = new ScrollingEntity(); - Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example.of(scrollingEntity, + ExampleMatcher.matchingAll().withIgnoreNullValues()); - var window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())); + var window = scrollingRepository.findBy(example, + q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0", "C0", "D0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("A0", "B0", "C0", "D0"); - ScrollPosition newPosition = ScrollPosition.forward(((KeysetScrollPosition) window.positionAt(window.size() - 1)).getKeys()); - window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(newPosition)); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("E0", "F0", "G0", "H0"); + ScrollPosition newPosition = ScrollPosition + .forward(((KeysetScrollPosition) window.positionAt(window.size() - 1)).getKeys()); + window = scrollingRepository.findBy(example, + q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(newPosition)); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("E0", "F0", "G0", "H0"); window = scrollingRepository.findTop4By(ScrollingEntity.SORT_BY_C, window.positionAt(window.size() - 1)); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("I0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("I0"); } @Test void backwardWithFluentQueryByExample(@Autowired ScrollingRepository repository) { ScrollingEntity scrollingEntity = new ScrollingEntity(); - Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example.of(scrollingEntity, + ExampleMatcher.matchingAll().withIgnoreNullValues()); var last = repository.findFirstByA("I0"); - var keys = Map.of( - "c", last.getC(), - Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) - ); + var keys = Map.of("c", last.getC(), Constants.NAME_OF_ADDITIONAL_SORT, + Values.value(last.getId().toString())); - var window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))); + var window = repository.findBy(example, + q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("F0", "G0", "H0", "I0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("F0", "G0", "H0", "I0"); var pos = ((KeysetScrollPosition) window.positionAt(0)); var nextPos = ScrollPosition.backward(pos.getKeys()); window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos)); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .extracting(ScrollingEntity::getA) - .containsExactly("B0", "C0", "D0", "E0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("B0", "C0", "D0", "E0"); var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos)); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("A0"); - } - } - - @Configuration - @EnableNeo4jRepositories - @EnableTransactionManagement - static class Config extends Neo4jImperativeTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @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)); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("A0"); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/StringlyTypedDynamicRelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/StringlyTypedDynamicRelationshipsIT.java index 4074158fd..e353c05f7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/StringlyTypedDynamicRelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/StringlyTypedDynamicRelationshipsIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,10 +24,10 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -45,12 +42,16 @@ import org.springframework.data.neo4j.integration.shared.common.Pet; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -64,7 +65,7 @@ class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase> hobbies = person.getHobbies(); assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getPerformance).containsExactly("average"); - assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getHobby).extracting(Hobby::getName).containsExactly("Biking"); + assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Biking"); } @Test // DATAGRAPH-1449 void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { - PersonWithStringlyTypedRelatives person = repository.findById(idOfExistingPerson).get(); + PersonWithStringlyTypedRelatives person = repository.findById(this.idOfExistingPerson).get(); assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -123,7 +126,7 @@ class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -253,16 +264,21 @@ class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(3L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -270,7 +286,7 @@ class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase> hobbies = person.getHobbies(); assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getPerformance).containsExactly("average"); - assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getHobby).extracting(Hobby::getName).containsExactly("Biking"); + assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Biking"); } interface PersonWithRelativesRepository extends CrudRepository { @Query("MATCH (p:PersonWithStringlyTypedRelatives)-[r] -> (o) WHERE id(p) = $personId return p, collect(r), collect(o)") PersonWithStringlyTypedRelatives byCustomQuery(@Param("personId") Long personId); + } @Configuration @@ -314,25 +333,30 @@ class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase - * While it does not integrate against a real database (multi-database is an enterprise feature), it is still an - * integration test due to the high integration with Spring framework code. + * While it does not integrate against a real database (multi-database is an enterprise + * feature), it is still an integration test due to the high integration with Spring + * framework code. * * @author Michael J. Simons */ @ExtendWith(SpringExtension.class) -class TransactionManagerMixedDatabasesTest { +class TransactionManagerMixedDatabasesTests { + + public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; protected static final String DATABASE_NAME = "boom"; - public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; private final Driver driver; private final TransactionTemplate transactionTemplate; @Autowired - TransactionManagerMixedDatabasesTest(Driver driver, Neo4jTransactionManager neo4jTransactionManager) { + TransactionManagerMixedDatabasesTests(Driver driver, Neo4jTransactionManager neo4jTransactionManager) { this.driver = driver; this.transactionTemplate = new TransactionTemplate(neo4jTransactionManager); @@ -99,12 +103,12 @@ class TransactionManagerMixedDatabasesTest { @Test void usingSameDatabaseExplicitTx(@Autowired Neo4jClient neo4jClient) { - Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(driver, + Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(this.driver, DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(DATABASE_NAME)); TransactionTemplate otherTransactionTemplate = new TransactionTemplate(otherTransactionManger); Optional numberOfNodes = otherTransactionTemplate - .execute(tx -> neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one()); + .execute(tx -> neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one()); assertThat(numberOfNodes).isPresent().hasValue(1L); } @@ -113,8 +117,10 @@ class TransactionManagerMixedDatabasesTest { void usingAnotherDatabaseDeclarative(@Autowired Neo4jClient neo4jClient) { assertThatIllegalStateException() - .isThrownBy(() -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one()) - .withMessage("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'"); + .isThrownBy( + () -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one()) + .withMessage( + "There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'"); } @@ -122,23 +128,25 @@ class TransactionManagerMixedDatabasesTest { void usingAnotherDatabaseExplicitTx(@Autowired Neo4jClient neo4jClient) { assertThatIllegalStateException() - .isThrownBy(() -> transactionTemplate - .execute(tx -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one())) - .withMessage("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'"); + .isThrownBy(() -> this.transactionTemplate.execute( + tx -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one())) + .withMessage( + "There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'"); } @Test void usingAnotherDatabaseDeclarativeFromRepo(@Autowired PersonRepository repository) { - Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(driver, + Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(this.driver, DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(DATABASE_NAME)); TransactionTemplate otherTransactionTemplate = new TransactionTemplate(otherTransactionManger); assertThatIllegalStateException() - .isThrownBy(() -> otherTransactionTemplate - .execute(tx -> repository.save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, - 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null)))) - .withMessage("There is already an ongoing Spring transaction for the default user of 'boom', but you requested the default user of the default database"); + .isThrownBy(() -> otherTransactionTemplate + .execute(tx -> repository.save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, + 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null)))) + .withMessage( + "There is already an ongoing Spring transaction for the default user of 'boom', but you requested the default user of the default database"); } @Configuration @@ -147,54 +155,57 @@ class TransactionManagerMixedDatabasesTest { static class Config extends AbstractNeo4jConfig { @Bean + @Override public Driver driver() { Record boomRecord = mock(Record.class); - when(boomRecord.size()).thenReturn(1); - when(boomRecord.get(0)).thenReturn(Values.value(1L)); + given(boomRecord.size()).willReturn(1); + given(boomRecord.get(0)).willReturn(Values.value(1L)); Record defaultRecord = mock(Record.class); - when(defaultRecord.size()).thenReturn(1); - when(defaultRecord.get(0)).thenReturn(Values.value(0L)); + given(defaultRecord.size()).willReturn(1); + given(defaultRecord.get(0)).willReturn(Values.value(0L)); Result boomResult = mock(Result.class); - when(boomResult.hasNext()).thenReturn(true); - when(boomResult.single()).thenReturn(boomRecord); - when(boomResult.consume()).thenReturn(mock(ResultSummary.class)); + given(boomResult.hasNext()).willReturn(true); + given(boomResult.single()).willReturn(boomRecord); + given(boomResult.consume()).willReturn(mock(ResultSummary.class)); Result defaultResult = mock(Result.class); - when(defaultResult.hasNext()).thenReturn(true); - when(defaultResult.single()).thenReturn(defaultRecord); - when(defaultResult.consume()).thenReturn(mock(ResultSummary.class)); + given(defaultResult.hasNext()).willReturn(true); + given(defaultResult.single()).willReturn(defaultRecord); + given(defaultResult.consume()).willReturn(mock(ResultSummary.class)); Transaction boomTransaction = mock(Transaction.class); - when(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomResult); - when(boomTransaction.isOpen()).thenReturn(true); + given(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).willReturn(boomResult); + given(boomTransaction.isOpen()).willReturn(true); Transaction defaultTransaction = mock(Transaction.class); - when(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultResult); - when(defaultTransaction.isOpen()).thenReturn(true); + given(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).willReturn(defaultResult); + given(defaultTransaction.isOpen()).willReturn(true); Session boomSession = mock(Session.class); - when(boomSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomResult); - when(boomSession.beginTransaction(any(TransactionConfig.class))).thenReturn(boomTransaction); - when(boomSession.isOpen()).thenReturn(true); + given(boomSession.run(eq(TEST_QUERY), any(Map.class))).willReturn(boomResult); + given(boomSession.beginTransaction(any(TransactionConfig.class))).willReturn(boomTransaction); + given(boomSession.isOpen()).willReturn(true); Session defaultSession = mock(Session.class); - when(defaultSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultResult); - when(defaultSession.beginTransaction(any(TransactionConfig.class))).thenReturn(defaultTransaction); - when(defaultSession.isOpen()).thenReturn(true); + given(defaultSession.run(eq(TEST_QUERY), any(Map.class))).willReturn(defaultResult); + given(defaultSession.beginTransaction(any(TransactionConfig.class))).willReturn(defaultTransaction); + given(defaultSession.isOpen()).willReturn(true); Driver driver = mock(Driver.class); - when(driver.session()).thenReturn(defaultSession); - when(driver.session(any(SessionConfig.class))).then(invocation -> { + given(driver.session()).willReturn(defaultSession); + given(driver.session(any(SessionConfig.class))).will(invocation -> { SessionConfig sessionConfig = invocation.getArgument(0); - return sessionConfig.database().map(n -> n.equals(DATABASE_NAME) ? boomSession : defaultSession) - .orElse(defaultSession); + return sessionConfig.database() + .map(n -> n.equals(DATABASE_NAME) ? boomSession : defaultSession) + .orElse(defaultSession); }); return driver; } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/FlightRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/FlightRepository.java index 2afc2d5d2..80b4336d4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/FlightRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/FlightRepository.java @@ -15,14 +15,16 @@ */ package org.springframework.data.neo4j.integration.imperative.repositories; +import java.util.List; + import org.springframework.data.neo4j.integration.shared.common.Flight; import org.springframework.data.neo4j.repository.Neo4jRepository; -import java.util.List; - /** * @author Michael J. Simons */ public interface FlightRepository extends Neo4jRepository { - List findAllByDepartureCodeAndArrivalCode(String departureCode, String arrivalCode); + + List findAllByDepartureCodeAndArrivalCode(String departureCode, String arrivalCode); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java index 41ec08ded..ac125c718 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.neo4j.driver.types.Point; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -61,22 +62,6 @@ public interface PersonRepository extends Neo4jRepository aggregateAllPeople(); - /** - * A custom aggregate that allows for something like getFriend1, 2 or other stuff... - */ - class CustomAggregation implements Streamable { - - private final Streamable delegate; - - public CustomAggregation(Streamable delegate) { - this.delegate = delegate; - } - - @Override public Iterator iterator() { - return delegate.iterator(); - } - } - @Query("MATCH (n:PersonWithAllConstructor) return collect(n)") CustomAggregation aggregateAllPeopleCustom(); @@ -121,15 +106,18 @@ public interface PersonRepository extends Neo4jRepository findTop1ByOrderByName(ScrollPosition scrollPosition); @Query("MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN n ORDER BY n.name DESC SKIP $skip LIMIT $limit") - Slice findSliceByCustomQueryWithoutCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable); + Slice findSliceByCustomQueryWithoutCount(@Param("aName") String aName, + @Param("anotherName") String anotherName, Pageable pageable); @Query(value = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN n ORDER BY n.name DESC SKIP $skip LIMIT $limit", - countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)") - Slice findSliceByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable); + countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)") + Slice findSliceByCustomQueryWithCount(@Param("aName") String aName, + @Param("anotherName") String anotherName, Pageable pageable); @Query(value = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN n :#{orderBy(#pageable)} SKIP $skip LIMIT $limit", countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)") - Page findPageByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable); + Page findPageByCustomQueryWithCount(@Param("aName") String aName, + @Param("anotherName") String anotherName, Pageable pageable); @Query("UNWIND ['a', 'b', 'c'] AS x RETURN x") List noDomainType(); @@ -225,12 +213,6 @@ public interface PersonRepository extends Neo4jRepository findAllByPlace(SomethingThatIsNotKnownAsEntity p); - /** - * Needed to have something that is not mapped in to a map. - */ - class SomethingThatIsNotKnownAsEntity { - } - List findAllByPlaceNear(Point p); List findAllByPlaceNear(Point p, Distance max); @@ -253,61 +235,39 @@ public interface PersonRepository extends Neo4jRepository findAllByOrderByFirstNameAscBornOnDesc(); + PersonProjection findByName(String name); + + List findBySameValue(String sameValue); + // TODO Integration tests for failed validations // List findAllByBornOnAfter(String date); - // List findAllByNameOrPersonNumberIsBetweenAndFirstNameNotInAndFirstNameEquals(String name, + // List + // findAllByNameOrPersonNumberIsBetweenAndFirstNameNotInAndFirstNameEquals(String + // name, // Long low, Long high, String wrong, List haystack); // List - // findAllByNameOrPersonNumberIsBetweenAndCoolIsTrueAndFirstNameNotInAndFirstNameEquals(String name, Long low, Long + // findAllByNameOrPersonNumberIsBetweenAndCoolIsTrueAndFirstNameNotInAndFirstNameEquals(String + // name, Long low, Long // high, String wrong, List haystack); // List findAllByNameNotEmpty(); // List findAllByPlaceNear(Point p); // List findAllByPlaceNear(Point p, String); - PersonProjection findByName(String name); - - List findBySameValue(String sameValue); - List findByFirstName(String firstName); Optional findOneByFirstName(String firstName); DtoPersonProjection findOneByNullable(String nullable); - @Query("" - + "MATCH (n:PersonWithAllConstructor) where n.name = $name " + @Query("" + "MATCH (n:PersonWithAllConstructor) where n.name = $name " + "WITH n MATCH(m:PersonWithAllConstructor) WHERE id(n) <> id(m) " + "RETURN n, collect(m) AS otherPeople, 4711 AS someLongValue, [21.42, 42.21] AS someDoubles") - List findAllDtoProjectionsWithAdditionalProperties(@Param("name") String name); + List findAllDtoProjectionsWithAdditionalProperties( + @Param("name") String name); - /** - * A custom aggregate that allows for something like getFriend1, 2 or other stuff... - */ - class CustomAggregationOfDto implements Streamable { - - private final Streamable delegate; - - public CustomAggregationOfDto(Streamable delegate) { - this.delegate = delegate; - } - - @Override public Iterator iterator() { - return delegate.iterator(); - } - - public DtoPersonProjectionContainingAdditionalFields getBySomeLongValue(long value) { - - return delegate.stream() - .collect(Collectors.toMap(DtoPersonProjectionContainingAdditionalFields::getSomeLongValue, Function.identity())) - .get(value); - } - } - - @Query("" - + "MATCH (n:PersonWithAllConstructor) where n.name = $name " - + "WITH n MATCH(m:PersonWithAllConstructor) WHERE id(n) <> id(m)" + - " WITH n, collect(m) as ms " - + "RETURN [{n: n, otherPeople: ms, someLongValue: 4711, someDoubles: [21.42, 42.21]}]") + @Query("" + "MATCH (n:PersonWithAllConstructor) where n.name = $name " + + "WITH n MATCH(m:PersonWithAllConstructor) WHERE id(n) <> id(m)" + " WITH n, collect(m) as ms " + + "RETURN [{n: n, otherPeople: ms, someLongValue: 4711, someDoubles: [21.42, 42.21]}]") CustomAggregationOfDto findAllDtoProjectionsWithAdditionalPropertiesAsCustomAggregation(@Param("name") String name); @Query("MATCH (n:PersonWithAllConstructor) where n.name = $name return n{.name}") @@ -325,10 +285,62 @@ public interface PersonRepository extends Neo4jRepository orderBySpel(Pageable page); void deleteAllByName(String name); long deleteAllByNameOrName(String name, String otherName); + + /** + * A custom aggregate that allows for something like getFriend1, 2 or other stuff... + */ + class CustomAggregation implements Streamable { + + private final Streamable delegate; + + public CustomAggregation(Streamable delegate) { + this.delegate = delegate; + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + } + + /** + * Needed to have something that is not mapped in to a map. + */ + class SomethingThatIsNotKnownAsEntity { + + } + + /** + * A custom aggregate that allows for something like getFriend1, 2 or other stuff... + */ + class CustomAggregationOfDto implements Streamable { + + private final Streamable delegate; + + public CustomAggregationOfDto(Streamable delegate) { + this.delegate = delegate; + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + public DtoPersonProjectionContainingAdditionalFields getBySomeLongValue(long value) { + + return this.delegate.stream() + .collect(Collectors.toMap(DtoPersonProjectionContainingAdditionalFields::getSomeLongValue, + Function.identity())) + .get(value); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java index e7b27574a..dfeb8718d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java @@ -35,4 +35,5 @@ public interface PersonWithNoConstructorRepository extends Neo4jRepository getOptionalPersonWithNoConstructorViaQuery(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java index d8e21f15b..8ca6fab3e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java @@ -25,7 +25,7 @@ import org.springframework.data.neo4j.repository.query.Query; /** * @author Michael J. Simons */ -public interface PersonWithWitherRepository extends Neo4jRepository { +public interface PersonWithWitherRepository extends Neo4jRepository { @Query("MATCH (n:PersonWithWither) return n") List getAllPersonsWithWitherViaQuery(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ScrollingRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ScrollingRepository.java index 8b45bc1c9..6a0316a6d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ScrollingRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ScrollingRepository.java @@ -36,4 +36,5 @@ public interface ScrollingRepository extends Neo4jRepository findAllByAOrderById(String a); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ThingRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ThingRepository.java index 7512b6732..6c0848a03 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ThingRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/ThingRepository.java @@ -25,10 +25,12 @@ import org.springframework.data.repository.CrudRepository; * @author Michael J. Simons */ public interface ThingRepository extends CrudRepository { + List findFirstByOrderByNameDesc(); List findTop5ByOrderByNameDesc(); @Query("MATCH (n:Thing{theId:'anId'})-[r:Has]->(b:Thing2) return n, collect(r), collect(b)") ThingWithAssignedId getViaQuery(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/package-info.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/package-info.java index 86cb6722a..a763d660b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/package-info.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * Repositories shared between tests. */ diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java index 5dd9aebbc..2004da75a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues; -import static org.assertj.core.api.Assertions.as; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.tuple; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -34,6 +28,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; +import ch.qos.logback.classic.Level; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.ThrowingConsumer; @@ -62,6 +57,7 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.TypeSystem; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -140,8 +136,8 @@ import org.springframework.data.neo4j.integration.issues.gh2533.EntitiesAndProje import org.springframework.data.neo4j.integration.issues.gh2533.GH2533Repository; import org.springframework.data.neo4j.integration.issues.gh2542.TestNode; import org.springframework.data.neo4j.integration.issues.gh2542.TestNodeRepository; -import org.springframework.data.neo4j.integration.issues.gh2572.GH2572Repository; import org.springframework.data.neo4j.integration.issues.gh2572.GH2572Child; +import org.springframework.data.neo4j.integration.issues.gh2572.GH2572Repository; import org.springframework.data.neo4j.integration.issues.gh2576.College; import org.springframework.data.neo4j.integration.issues.gh2576.CollegeRepository; import org.springframework.data.neo4j.integration.issues.gh2576.Student; @@ -162,6 +158,13 @@ import org.springframework.data.neo4j.integration.issues.gh2639.Individual; import org.springframework.data.neo4j.integration.issues.gh2639.LanguageRelationship; import org.springframework.data.neo4j.integration.issues.gh2639.ProgrammingLanguage; import org.springframework.data.neo4j.integration.issues.gh2639.Sales; +import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelEntity; +import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelEntityRepository; +import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelProjection; +import org.springframework.data.neo4j.integration.issues.gh2727.SecondLevelEntity; +import org.springframework.data.neo4j.integration.issues.gh2727.SecondLevelEntityRelationship; +import org.springframework.data.neo4j.integration.issues.gh2727.ThirdLevelEntity; +import org.springframework.data.neo4j.integration.issues.gh2727.ThirdLevelEntityRelationship; import org.springframework.data.neo4j.integration.issues.gh2819.GH2819Model; import org.springframework.data.neo4j.integration.issues.gh2819.GH2819Repository; import org.springframework.data.neo4j.integration.issues.gh2858.GH2858; @@ -199,13 +202,6 @@ import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipD; import org.springframework.data.neo4j.integration.issues.qbe.A; import org.springframework.data.neo4j.integration.issues.qbe.ARepository; import org.springframework.data.neo4j.integration.issues.qbe.B; -import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelEntity; -import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelEntityRepository; -import org.springframework.data.neo4j.integration.issues.gh2727.FirstLevelProjection; -import org.springframework.data.neo4j.integration.issues.gh2727.SecondLevelEntity; -import org.springframework.data.neo4j.integration.issues.gh2727.SecondLevelEntityRelationship; -import org.springframework.data.neo4j.integration.issues.gh2727.ThirdLevelEntity; -import org.springframework.data.neo4j.integration.issues.gh2727.ThirdLevelEntityRelationship; import org.springframework.data.neo4j.integration.misc.ConcreteImplementationTwo; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; @@ -218,11 +214,14 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import ch.qos.logback.classic.Level; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.tuple; /** * @author Michael J. Simons - * @soundtrack Sodom - Sodom */ @Neo4jIntegrationTest @DisplayNameGeneration(SimpleDisplayNameGeneratorWithTags.class) @@ -232,8 +231,11 @@ class IssuesIT extends TestBase { // GH-2210 private static final Long numberA = 1L; + private static final Long numberB = 2L; + private static final Long numberC = 3L; + private static final Long numberD = 4L; // GH-2323 @@ -244,8 +246,9 @@ class IssuesIT extends TestBase { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { if (neo4jConnectionSupport.isCypher5SyntaxCompatible()) { session.run("CREATE CONSTRAINT TNC IF NOT EXISTS FOR (tn:TestNode) REQUIRE tn.name IS UNIQUE") - .consume(); - } else { + .consume(); + } + else { session.run("CREATE CONSTRAINT TNC IF NOT EXISTS ON (tn:TestNode) ASSERT tn.name IS UNIQUE").consume(); } try (Transaction transaction = session.beginTransaction()) { @@ -261,7 +264,8 @@ class IssuesIT extends TestBase { setupGH2583(transaction); setupGH2908(transaction); - transaction.run("CREATE (:A {name: 'A name', id: randomUUID()}) -[:HAS] ->(:B {anotherName: 'Whatever', id: randomUUID()})"); + transaction.run( + "CREATE (:A {name: 'A name', id: randomUUID()}) -[:HAS] ->(:B {anotherName: 'Whatever', id: randomUUID()})"); transaction.commit(); } @@ -271,53 +275,29 @@ class IssuesIT extends TestBase { // clean up known throw-away nodes / rels - @AfterEach - void cleanup(@Autowired BookmarkCapture bookmarkCapture) { - List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); - var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); - for (int i = 1; i < labelsToBeRemoved.size(); i++) { - labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); - } - try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction()) { - Node nodes = Cypher.node(labelExpression); - String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); - transaction.run(cypher).consume(); - transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - private static void setupGH2168(QueryRunner queryRunner) { queryRunner.run("CREATE (:DomainObject{id: 'A'})").consume(); } private static void setupGH2210(QueryRunner queryRunner) { queryRunner.run(""" - create (a:SomeEntity {number: $numberA, name: "A"}) - create (b:SomeEntity {number: $numberB, name: "B"}) - create (c:SomeEntity {number: $numberC, name: "C"}) - create (d:SomeEntity {number: $numberD, name: "D"}) - create (a) -[:SOME_RELATION_TO {someData: "d1"}] -> (b) - create (b) <-[:SOME_RELATION_TO {someData: "d2"}] - (c) - create (c) <-[:SOME_RELATION_TO {someData: "d3"}] - (d) - return *""", - Map.of("numberA", numberA, - "numberB", numberB, - "numberC", numberC, - "numberD", numberD)).consume(); + create (a:SomeEntity {number: $numberA, name: "A"}) + create (b:SomeEntity {number: $numberB, name: "B"}) + create (c:SomeEntity {number: $numberC, name: "C"}) + create (d:SomeEntity {number: $numberD, name: "D"}) + create (a) -[:SOME_RELATION_TO {someData: "d1"}] -> (b) + create (b) <-[:SOME_RELATION_TO {someData: "d2"}] - (c) + create (c) <-[:SOME_RELATION_TO {someData: "d3"}] - (d) + return *""", Map.of("numberA", numberA, "numberB", numberB, "numberC", numberC, "numberD", numberD)) + .consume(); } private static void setupGH2323(QueryRunner queryRunner) { - queryRunner.run("unwind ['German', 'English'] as name create (n:Language {name: name}) return name") - .consume(); + queryRunner.run("unwind ['German', 'English'] as name create (n:Language {name: name}) return name").consume(); personId = queryRunner.run(""" - MATCH (l:Language {name: 'German'}) - CREATE (n:Person {id: randomUUID(), name: 'Helge'}) -[:HAS_MOTHER_TONGUE]-> (l) - return n.id""" - ).single() - .get(0) - .asString(); + MATCH (l:Language {name: 'German'}) + CREATE (n:Person {id: randomUUID(), name: 'Helge'}) -[:HAS_MOTHER_TONGUE]-> (l) + return n.id""").single().get(0).asString(); } private static void setupGH2459(QueryRunner queryRunner) { @@ -344,10 +324,179 @@ class IssuesIT extends TestBase { -[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)""").consume(); } + private static void assertGH2905Graph(Driver driver) { + var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f") + .execute() + .records(); + assertThat(result).hasSize(1).element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .hasSize(3); + }); + } + + private static void assertGH2906Graph(Driver driver) { + assertGH2906Graph(driver, 3); + } + + private static void assertGH2906Graph(Driver driver, int cnt) { + + var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); + var expectedRelationships = IntStream.rangeClosed(1, cnt) + .mapToObj(i -> String.format("F%d<-T1", i)) + .toArray(String[]::new); + + var result = driver + .executableQuery( + "MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r") + .execute() + .records(); + assertThat(result).hasSize(1).element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) + .containsExactlyInAnyOrder(expectedNodes); + assertThat(r.get("r")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(rel -> ((Relationship) rel).get("comment").asString()) + .containsExactlyInAnyOrder(expectedRelationships); + }); + } + + private static void assertWriteAndReadConversionForProperty(Neo4jPersistentEntity entity, String propertyName, + DomainObjectRepository repository, Driver driver, BookmarkCapture bookmarkCapture) { + Neo4jPersistentProperty property = entity.getPersistentProperty(propertyName); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(new DomainObject()); + + propertyAccessor.setProperty(property, new UnrelatedObject(true, 4711L)); + DomainObject domainObject = repository.save(propertyAccessor.getBean()); + + try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + var node = session + .run("MATCH (n:DomainObject {id: $id}) RETURN n", Collections.singletonMap("id", domainObject.getId())) + .single() + .get(0) + .asNode(); + assertThat(node.get(propertyName).asString()).isEqualTo("true;4711"); + } + + domainObject = repository.findById(domainObject.getId()).get(); + UnrelatedObject unrelatedObject = (UnrelatedObject) entity.getPropertyAccessor(domainObject) + .getProperty(property); + assertThat(unrelatedObject).satisfies(t -> { + assertThat(t.isABooleanValue()).isTrue(); + assertThat(t.getALongValue()).isEqualTo(4711L); + }); + } + + private static void assertAll(List entities) { + + assertThat(entities).hasSize(4); + assertThat(entities).allSatisfy(v -> { + switch (v.getName()) { + case "A" -> assertA(Optional.of(v)); + case "B" -> assertB(Optional.of(v)); + case "D" -> assertD(Optional.of(v)); + } + }); + } + + private static void assertA(Optional a) { + + assertThat(a).hasValueSatisfying(s -> { + assertThat(s.getName()).isEqualTo("A"); + assertThat(s.getSomeRelationsOut()).hasSize(1).first().satisfies(b -> { + assertThat(b.getSomeData()).isEqualTo("d1"); + assertThat(b.getTargetPerson().getName()).isEqualTo("B"); + assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty(); + }); + }); + } + + private static void assertD(Optional d) { + + assertThat(d).hasValueSatisfying(s -> { + assertThat(s.getName()).isEqualTo("D"); + assertThat(s.getSomeRelationsOut()).hasSize(1).first().satisfies(c -> { + assertThat(c.getSomeData()).isEqualTo("d3"); + assertThat(c.getTargetPerson().getName()).isEqualTo("C"); + assertThat(c.getTargetPerson().getSomeRelationsOut()).hasSize(1).first().satisfies(b -> { + assertThat(b.getSomeData()).isEqualTo("d2"); + assertThat(b.getTargetPerson().getName()).isEqualTo("B"); + assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty(); + }); + }); + }); + } + + private static void assertB(Optional b) { + + assertThat(b).hasValueSatisfying(s -> { + assertThat(s.getName()).isEqualTo("B"); + assertThat(s.getSomeRelationsOut()).isEmpty(); + }); + } + + private static void assertThatTestObjectHasBeenCreated(Driver driver, BookmarkCapture bookmarkCapture, + TestObject testObject) { + try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + Map arguments = new HashMap<>(); + arguments.put("id", testObject.getId()); + arguments.put("num", testObject.getData().getNum()); + arguments.put("string", testObject.getData().getString()); + long cnt = session.run( + "MATCH (n:TestObject) WHERE n.id = $id AND n.dataNum = $num AND n.dataString = $string RETURN count(n)", + arguments) + .single() + .get(0) + .asLong(); + assertThat(cnt).isOne(); + } + } + + private static EntitiesAndProjections.GH2533Entity createData(GH2533Repository repository) { + EntitiesAndProjections.GH2533Entity n1 = new EntitiesAndProjections.GH2533Entity(); + EntitiesAndProjections.GH2533Entity n2 = new EntitiesAndProjections.GH2533Entity(); + EntitiesAndProjections.GH2533Entity n3 = new EntitiesAndProjections.GH2533Entity(); + + EntitiesAndProjections.GH2533Relationship r1 = new EntitiesAndProjections.GH2533Relationship(); + EntitiesAndProjections.GH2533Relationship r2 = new EntitiesAndProjections.GH2533Relationship(); + + n1.name = "n1"; + n2.name = "n2"; + n3.name = "n3"; + + r1.target = n2; + r2.target = n3; + + n1.relationships = Collections.singletonMap("has_relationship_with", List.of(r1)); + n2.relationships = Collections.singletonMap("has_relationship_with", List.of(r2)); + + return repository.save(n1); + } + + @AfterEach + void cleanup(@Autowired BookmarkCapture bookmarkCapture) { + List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", + "BugTargetBase", "BugTargetContainer"); + var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); + for (int i = 1; i < labelsToBeRemoved.size(); i++) { + labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); + } + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + Node nodes = Cypher.node(labelExpression); + String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); + transaction.run(cypher).consume(); + transaction.commit(); + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + @BeforeEach - protected void prepareIndividual( - @Autowired CityModelRepository cityModelRepository - ) { + protected void prepareIndividual(@Autowired CityModelRepository cityModelRepository) { CityModel aachen = new CityModel(); aachen.setName("Aachen"); aachen.setExoticProperty("Cars"); @@ -381,24 +530,23 @@ class IssuesIT extends TestBase { void findByIdShouldWork(@Autowired DomainObjectRepository domainObjectRepository) { Optional optionalResult = domainObjectRepository.findById("A"); - assertThat(optionalResult) - .map(DomainObject::getId) - .hasValue("A"); + assertThat(optionalResult).map(DomainObject::getId).hasValue("A"); } @Test @Tag("GH-2415") void saveWithProjectionImplementedByEntity(@Autowired Neo4jMappingContext mappingContext, - @Autowired Neo4jTemplate neo4jTemplate) { + @Autowired Neo4jTemplate neo4jTemplate) { Neo4jPersistentEntity metaData = mappingContext.getPersistentEntity(BaseNodeEntity.class); - NodeEntity nodeEntity = neo4jTemplate - .find(BaseNodeEntity.class) - .as(NodeEntity.class) - .matching(QueryFragmentsAndParameters.forCondition(metaData, - Constants.NAME_OF_TYPED_ROOT_NODE.apply(metaData).property("nodeId") - .isEqualTo(Cypher.literalOf("root")))) - .one().get(); + NodeEntity nodeEntity = neo4jTemplate.find(BaseNodeEntity.class) + .as(NodeEntity.class) + .matching(QueryFragmentsAndParameters.forCondition(metaData, + Constants.NAME_OF_TYPED_ROOT_NODE.apply(metaData) + .property("nodeId") + .isEqualTo(Cypher.literalOf("root")))) + .one() + .get(); neo4jTemplate.saveAs(nodeEntity, NodeWithDefinedCredentials.class); nodeEntity = neo4jTemplate.findById(nodeEntity.getNodeId(), NodeEntity.class).get(); @@ -407,11 +555,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2168") - void compositePropertyCustomConverterDefaultPrefixShouldWork( - @Autowired DomainObjectRepository repository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + void compositePropertyCustomConverterDefaultPrefixShouldWork(@Autowired DomainObjectRepository repository, + @Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { DomainObject domainObject = new DomainObject(); domainObject.setStoredAsMultipleProperties(new UnrelatedObject(true, 4711L)); @@ -419,43 +564,38 @@ class IssuesIT extends TestBase { try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { var node = session - .run("MATCH (n:DomainObject {id: $id}) RETURN n", - Collections.singletonMap("id", domainObject.getId())) - .single().get(0).asNode(); + .run("MATCH (n:DomainObject {id: $id}) RETURN n", Collections.singletonMap("id", domainObject.getId())) + .single() + .get(0) + .asNode(); assertThat(node.get("storedAsMultipleProperties.aBooleanValue").asBoolean()).isTrue(); assertThat(node.get("storedAsMultipleProperties.aLongValue").asLong()).isEqualTo(4711L); } domainObject = repository.findById(domainObject.getId()).get(); - assertThat(domainObject.getStoredAsMultipleProperties()) - .satisfies(t -> { - assertThat(t.isABooleanValue()).isTrue(); - assertThat(t.getALongValue()).isEqualTo(4711L); - }); + assertThat(domainObject.getStoredAsMultipleProperties()).satisfies(t -> { + assertThat(t.isABooleanValue()).isTrue(); + assertThat(t.getALongValue()).isEqualTo(4711L); + }); } - // That test and the underlying mapping cause the original issue to fail, as `@ConvertWith` was missing for non-simple + // That test and the underlying mapping cause the original issue to fail, as + // `@ConvertWith` was missing for non-simple // types in the lookup that checked whether something is an association or not @Test @Tag("GH-2168") - void propertyCustomConverterDefaultPrefixShouldWork( - @Autowired Neo4jMappingContext ctx, - @Autowired DomainObjectRepository repository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + void propertyCustomConverterDefaultPrefixShouldWork(@Autowired Neo4jMappingContext ctx, + @Autowired DomainObjectRepository repository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { Neo4jPersistentEntity entity = ctx.getRequiredPersistentEntity(DomainObject.class); assertWriteAndReadConversionForProperty(entity, "storedAsSingleProperty", repository, driver, bookmarkCapture); } @Test @Tag("GH-2430") - void propertyConversionsWithBeansShouldWork( - @Autowired Neo4jMappingContext ctx, - @Autowired DomainObjectRepository repository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + void propertyConversionsWithBeansShouldWork(@Autowired Neo4jMappingContext ctx, + @Autowired DomainObjectRepository repository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { Neo4jPersistentEntity entity = ctx.getRequiredPersistentEntity(DomainObject.class); assertWriteAndReadConversionForProperty(entity, "storedAsAnotherSingleProperty", repository, driver, bookmarkCapture); @@ -627,12 +767,14 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2326") void saveShouldAddAllLabels(@Autowired AnimalRepository animalRepository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { List animals = Arrays.asList(new AbstractLevel2.AbstractLevel3.Concrete1(), new AbstractLevel2.AbstractLevel3.Concrete2()); - List ids = animals.stream().map(animalRepository::save).map(BaseEntity::getId) - .collect(Collectors.toList()); + List ids = animals.stream() + .map(animalRepository::save) + .map(BaseEntity::getId) + .collect(Collectors.toList()); assertLabels(bookmarkCapture, ids); } @@ -640,12 +782,14 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2326") void saveAllShouldAddAllLabels(@Autowired AnimalRepository animalRepository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { List animals = Arrays.asList(new AbstractLevel2.AbstractLevel3.Concrete1(), new AbstractLevel2.AbstractLevel3.Concrete2()); - List ids = animalRepository.saveAll(animals).stream().map(BaseEntity::getId) - .collect(Collectors.toList()); + List ids = animalRepository.saveAll(animals) + .stream() + .map(BaseEntity::getId) + .collect(Collectors.toList()); assertLabels(bookmarkCapture, ids); } @@ -660,10 +804,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2347") void entitiesWithAssignedIdsSavedInBatchMustBeIdentifiableWithTheirInternalIds( - @Autowired ApplicationRepository applicationRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired ApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { List savedApplications = applicationRepository.saveAll(Collections.singletonList(createData())); assertThat(savedApplications).hasSize(1); @@ -673,10 +815,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2347") void entitiesWithAssignedIdsMustBeIdentifiableWithTheirInternalIds( - @Autowired ApplicationRepository applicationRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired ApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { applicationRepository.save(createData()); assertSingleApplicationNodeWithMultipleWorkflows(driver, bookmarkCapture); } @@ -684,10 +824,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2346") void relationshipsStartingAtEntitiesWithAssignedIdsShouldBeCreated( - @Autowired ApplicationRepository applicationRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired ApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { createData((applications, workflows) -> { List savedApplications = applicationRepository.saveAll(applications); @@ -699,10 +837,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2346") void relationshipsStartingAtEntitiesWithAssignedIdsShouldBeCreatedOtherDirection( - @Autowired WorkflowRepository workflowRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired WorkflowRepository workflowRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { createData((applications, workflows) -> { List savedWorkflows = workflowRepository.saveAll(workflows); @@ -716,21 +852,19 @@ class IssuesIT extends TestBase { void dontOverrideAbstractMappedData(@Autowired PetOwnerRepository repository) { Optional optionalPetOwner = repository.findById("10"); assertThat(optionalPetOwner).isPresent() - .hasValueSatisfying(petOwner -> - assertThat(petOwner.getPets()).hasSize(2)); + .hasValueSatisfying(petOwner -> assertThat(petOwner.getPets()).hasSize(2)); } @Test @Tag("GH-2474") - public void testStoreExoticProperty(@Autowired CityModelRepository cityModelRepository) { + void testStoreExoticProperty(@Autowired CityModelRepository cityModelRepository) { CityModel cityModel = new CityModel(); cityModel.setName("The Jungle"); cityModel.setExoticProperty("lions"); cityModel = cityModelRepository.save(cityModel); - CityModel reloaded = cityModelRepository.findById(cityModel.getCityId()) - .orElseThrow(RuntimeException::new); + CityModel reloaded = cityModelRepository.findById(cityModel.getCityId()).orElseThrow(RuntimeException::new); assertThat(reloaded.getExoticProperty()).isEqualTo("lions"); long cnt = cityModelRepository.deleteAllByExoticProperty("lions"); @@ -739,7 +873,7 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2474") - public void testSortOnExoticProperty(@Autowired CityModelRepository cityModelRepository) { + void testSortOnExoticProperty(@Autowired CityModelRepository cityModelRepository) { Sort sort = Sort.by(Sort.Order.asc("exoticProperty")); List cityModels = cityModelRepository.findAll(sort); @@ -749,8 +883,7 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2474") - public void testSortOnExoticPropertyCustomQuery_MakeSureIUnderstand( - @Autowired CityModelRepository cityModelRepository) { + void testSortOnExoticPropertyCustomQuery_MakeSureIUnderstand(@Autowired CityModelRepository cityModelRepository) { Sort sort = Sort.by(Sort.Order.asc("n.name")); List cityModels = cityModelRepository.customQuery(sort); @@ -760,7 +893,7 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2474") - public void testSortOnExoticPropertyCustomQuery(@Autowired CityModelRepository cityModelRepository) { + void testSortOnExoticPropertyCustomQuery(@Autowired CityModelRepository cityModelRepository) { Sort sort = Sort.by(Sort.Order.asc("n.`exotic.property`")); List cityModels = cityModelRepository.customQuery(sort); @@ -769,11 +902,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2475") - public void testCityModelProjectionPersistence( - @Autowired CityModelRepository cityModelRepository, - @Autowired PersonModelRepository personModelRepository, - @Autowired Neo4jTemplate neo4jTemplate - ) { + void testCityModelProjectionPersistence(@Autowired CityModelRepository cityModelRepository, + @Autowired PersonModelRepository personModelRepository, @Autowired Neo4jTemplate neo4jTemplate) { CityModel cityModel = new CityModel(); cityModel.setName("New Cool City"); cityModel = cityModelRepository.save(cityModel); @@ -785,7 +915,7 @@ class IssuesIT extends TestBase { personModelRepository.save(personModel); CityModelDTO cityModelDTO = cityModelRepository.findByCityId(cityModel.getCityId()) - .orElseThrow(RuntimeException::new); + .orElseThrow(RuntimeException::new); cityModelDTO.setName("Changed name"); cityModelDTO.setExoticProperty("tigers"); @@ -800,8 +930,7 @@ class IssuesIT extends TestBase { cityModelDTO.setCityEmployees(Collections.singletonList(jobRelationshipDTO)); neo4jTemplate.save(CityModel.class).one(cityModelDTO); - CityModel reloaded = cityModelRepository.findById(cityModel.getCityId()) - .orElseThrow(RuntimeException::new); + CityModel reloaded = cityModelRepository.findById(cityModel.getCityId()).orElseThrow(RuntimeException::new); assertThat(reloaded.getName()).isEqualTo("Changed name"); assertThat(reloaded.getMayor()).isNotNull(); assertThat(reloaded.getCitizens()).hasSize(1); @@ -822,7 +951,6 @@ class IssuesIT extends TestBase { assertThat(models).extracting("name").containsExactly("Aachen", "Utrecht"); } - @Test @Tag("GH-2884") void sortByCompositePropertyForCyclicDomainReturn(@Autowired SkuRORepository repository) { @@ -834,7 +962,7 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2493") void saveOneShouldWork(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture, - @Autowired TestObjectRepository repository) { + @Autowired TestObjectRepository repository) { TestObject testObject = new TestObject(new TestData(4711, "Foobar")); testObject = repository.save(testObject); @@ -846,7 +974,7 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2493") void saveAllShouldWork(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture, - @Autowired TestObjectRepository repository) { + @Autowired TestObjectRepository repository) { TestObject testObject = new TestObject(new TestData(4711, "Foobar")); testObject = repository.saveAll(Collections.singletonList(testObject)).get(0); @@ -864,9 +992,7 @@ class IssuesIT extends TestBase { Parameter> parameters = Cypher.anonParameter(List.of("A", "C")); Condition in = name.in(parameters); Collection result = repository.findAll(in, Cypher.sort(name).descending()); - assertThat(result).hasSize(2) - .map(DomainModel::getName) - .containsExactly("C", "A"); + assertThat(result).hasSize(2).map(DomainModel::getName).containsExactly("C", "A"); } @Test @@ -877,9 +1003,7 @@ class IssuesIT extends TestBase { Parameter> param = Cypher.anonParameter(List.of("a", "b")); Condition in = name.in(param); Collection people = repository.findAll(in); - assertThat(people) - .extracting(Vertex::getName) - .containsExactlyInAnyOrder("a", "b"); + assertThat(people).extracting(Vertex::getName).containsExactlyInAnyOrder("a", "b"); } @Test @@ -893,10 +1017,12 @@ class IssuesIT extends TestBase { template.save(group); try (Session session = driver.session()) { - long cnt = session.run( - "MATCH (g:Group {name: $name}) <-[:BELONGS_TO]- (d:Device {id: $deviceId}) RETURN count(*)", - Map.of("name", group.getName(), "deviceId", 1L)) - .single().get(0).asLong(); + long cnt = session + .run("MATCH (g:Group {name: $name}) <-[:BELONGS_TO]- (d:Device {id: $deviceId}) RETURN count(*)", + Map.of("name", group.getName(), "deviceId", 1L)) + .single() + .get(0) + .asLong(); assertThat(cnt).isOne(); } } @@ -907,9 +1033,10 @@ class IssuesIT extends TestBase { MeasurementProjection m = repository.findByNodeId("acc1", MeasurementProjection.class); assertThat(m).isNotNull(); assertThat(m.getDataPoints()).isNotEmpty(); - assertThat(m).extracting(MeasurementProjection::getDataPoints, - InstanceOfAssertFactories.collection(DataPoint.class)) - .extracting(DataPoint::isManual, DataPoint::getMeasurand).contains(tuple(true, new Measurand("o1"))); + assertThat(m) + .extracting(MeasurementProjection::getDataPoints, InstanceOfAssertFactories.collection(DataPoint.class)) + .extracting(DataPoint::isManual, DataPoint::getMeasurand) + .contains(tuple(true, new Measurand("o1"))); } @Test @@ -935,16 +1062,18 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2533") void projectionWorksForDynamicRelationshipsOnSave(@Autowired GH2533Repository repository, - @Autowired Neo4jTemplate neo4jTemplate) { + @Autowired Neo4jTemplate neo4jTemplate) { EntitiesAndProjections.GH2533Entity rootEntity = createData(repository); rootEntity = repository.findByIdWithLevelOneLinks(rootEntity.id).get(); - // this had caused the rootEntity -> child -X-> child relationship to get removed (X). + // this had caused the rootEntity -> child -X-> child relationship to get removed + // (X). neo4jTemplate.saveAs(rootEntity, EntitiesAndProjections.GH2533EntityNodeWithOneLevelLinks.class); - EntitiesAndProjections.GH2533Entity entity = neo4jTemplate.findById(rootEntity.id, - EntitiesAndProjections.GH2533Entity.class).get(); + EntitiesAndProjections.GH2533Entity entity = neo4jTemplate + .findById(rootEntity.id, EntitiesAndProjections.GH2533Entity.class) + .get(); assertThat(entity.relationships).isNotEmpty(); assertThat(entity.relationships.get("has_relationship_with")).isNotEmpty(); @@ -955,17 +1084,19 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2533") void saveRelatedEntityWithRelationships(@Autowired GH2533Repository repository, - @Autowired Neo4jTemplate neo4jTemplate) { + @Autowired Neo4jTemplate neo4jTemplate) { EntitiesAndProjections.GH2533Entity rootEntity = createData(repository); neo4jTemplate.saveAs(rootEntity, EntitiesAndProjections.GH2533EntityWithRelationshipToEntity.class); - EntitiesAndProjections.GH2533Entity entity = neo4jTemplate.findById(rootEntity.id, - EntitiesAndProjections.GH2533Entity.class).get(); + EntitiesAndProjections.GH2533Entity entity = neo4jTemplate + .findById(rootEntity.id, EntitiesAndProjections.GH2533Entity.class) + .get(); assertThat(entity.relationships.get("has_relationship_with").get(0).target.name).isEqualTo("n2"); - assertThat(entity.relationships.get("has_relationship_with").get(0).target.relationships.get( - "has_relationship_with").get(0).target.name).isEqualTo("n3"); + assertThat(entity.relationships.get("has_relationship_with").get(0).target.relationships + .get("has_relationship_with") + .get(0).target.name).isEqualTo("n3"); } @Test @@ -974,8 +1105,7 @@ class IssuesIT extends TestBase { repository.save(new TestNode("Bob")); var secondNode = new TestNode("Bob"); - assertThatExceptionOfType(DataIntegrityViolationException.class) - .isThrownBy(() -> repository.save(secondNode)); + assertThatExceptionOfType(DataIntegrityViolationException.class).isThrownBy(() -> repository.save(secondNode)); } @Test @@ -1022,7 +1152,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2576") - void listOfMapsShouldBeUsableAsArguments(@Autowired Neo4jTemplate template, @Autowired CollegeRepository collegeRepository) { + void listOfMapsShouldBeUsableAsArguments(@Autowired Neo4jTemplate template, + @Autowired CollegeRepository collegeRepository) { var student = template.save(new Student("S1")); var college = template.save(new College("C1")); @@ -1036,7 +1167,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2576") - void listOfMapsShouldBeUsableAsArgumentsWithWorkaround(@Autowired Neo4jTemplate template, @Autowired CollegeRepository collegeRepository) { + void listOfMapsShouldBeUsableAsArgumentsWithWorkaround(@Autowired Neo4jTemplate template, + @Autowired CollegeRepository collegeRepository) { var student = template.save(new Student("S1")); var college = template.save(new College("C1")); @@ -1075,14 +1207,11 @@ class IssuesIT extends TestBase { tableRepository.mergeTableAndColumnRelations(List.of(c1, c2), tableNode); Optional resolvedTableNode = tableRepository.findById(tableNode.getId()); - assertThat(resolvedTableNode) - .map(TableNode::getTableAndColumnRelation) - .hasValueSatisfying(l -> { - assertThat(l) - .map(TableAndColumnRelation::getColumnNode) - .map(ColumnNode::getId) - .containsExactlyInAnyOrder(c1Id, c2Id); - }); + assertThat(resolvedTableNode).map(TableNode::getTableAndColumnRelation).hasValueSatisfying(l -> { + assertThat(l).map(TableAndColumnRelation::getColumnNode) + .map(ColumnNode::getId) + .containsExactlyInAnyOrder(c1Id, c2Id); + }); } @Test @@ -1119,24 +1248,20 @@ class IssuesIT extends TestBase { Company loadedAcme = companyRepository.findByName("ACME"); - Developer loadedHarry = loadedAcme.getEmployees().stream() - .filter(e -> e instanceof Developer) - .map(e -> (Developer) e) - .filter(developer -> developer.getName().equals("Harry")) - .findFirst().get(); + Developer loadedHarry = loadedAcme.getEmployees() + .stream() + .filter(e -> e instanceof Developer) + .map(e -> (Developer) e) + .filter(developer -> developer.getName().equals("Harry")) + .findFirst() + .get(); List programmingLanguages = loadedHarry.getProgrammingLanguages(); - assertThat(programmingLanguages) - .isNotEmpty() - .extracting("score") - .containsExactlyInAnyOrder(5, 2); + assertThat(programmingLanguages).isNotEmpty().extracting("score").containsExactlyInAnyOrder(5, 2); - assertThat(programmingLanguages) - .extracting("language") - .extracting("inventor") - .containsExactlyInAnyOrder( - new Individual("Larry Wall", "larryW"), new Enterprise("Sun", ";(") - ); + assertThat(programmingLanguages).extracting("language") + .extracting("inventor") + .containsExactlyInAnyOrder(new Individual("Larry Wall", "larryW"), new Enterprise("Sun", ";(")); } @Test @@ -1147,15 +1272,16 @@ class IssuesIT extends TestBase { repository.save(entity); assertThatExceptionOfType(MappingException.class).isThrownBy(repository::findAll) - .withRootCauseInstanceOf(MappingException.class) - .extracting(Throwable::getCause, as(InstanceOfAssertFactories.THROWABLE)) - .hasMessageContaining("has a logical cyclic mapping dependency"); + .withRootCauseInstanceOf(MappingException.class) + .extracting(Throwable::getCause, as(InstanceOfAssertFactories.THROWABLE)) + .hasMessageContaining("has a logical cyclic mapping dependency"); } @Test @Tag("GH-2727") - void mapsProjectionChainWithRelationshipProperties(@Autowired FirstLevelEntityRepository firstLevelEntityRepository) { + void mapsProjectionChainWithRelationshipProperties( + @Autowired FirstLevelEntityRepository firstLevelEntityRepository) { String secondLevelValue = "someSecondLevelValue"; String thirdLevelValue = "someThirdLevelValue"; @@ -1173,9 +1299,9 @@ class IssuesIT extends TestBase { } final SecondLevelEntity secondLevelEntity = SecondLevelEntity.builder() - .thirdLevelEntityRelationshipProperties(thirdLevelEntityRelationships) - .someValue(secondLevelValue) - .build(); + .thirdLevelEntityRelationshipProperties(thirdLevelEntityRelationships) + .someValue(secondLevelValue) + .build(); final SecondLevelEntityRelationship secondLevelRelationship = new SecondLevelEntityRelationship(); secondLevelRelationship.setTarget(secondLevelEntity); @@ -1184,33 +1310,35 @@ class IssuesIT extends TestBase { } final FirstLevelEntity firstLevelEntity = FirstLevelEntity.builder() - .secondLevelEntityRelationshipProperties(secondLevelEntityRelationships) - .name("Test") - .build(); + .secondLevelEntityRelationshipProperties(secondLevelEntityRelationships) + .name("Test") + .build(); firstLevelEntityRepository.save(firstLevelEntity); FirstLevelProjection firstLevelProjection = firstLevelEntityRepository.findOneById(firstLevelEntity.getId()); assertThat(firstLevelProjection).isNotNull(); assertThat(firstLevelProjection.getSecondLevelEntityRelationshipProperties()).hasSize(2) - .allSatisfy(secondLevelRelationship -> { - assertThat(secondLevelRelationship.getTarget().getSomeValue().equals(secondLevelValue)); - assertThat(secondLevelRelationship.getOrder()).isGreaterThan(0); - assertThat(secondLevelRelationship.getTarget().getThirdLevelEntityRelationshipProperties()) - .isNotEmpty() - .allSatisfy(thirdLevel -> { - assertThat(thirdLevel.getOrder()).isGreaterThan(0); - assertThat(thirdLevel.getTarget()).isNotNull(); - assertThat(thirdLevel.getTarget().getSomeValue()).isEqualTo(thirdLevelValue); - }); - }); + .allSatisfy(secondLevelRelationship -> { + assertThat(secondLevelRelationship.getTarget().getSomeValue().equals(secondLevelValue)); + assertThat(secondLevelRelationship.getOrder()).isGreaterThan(0); + assertThat(secondLevelRelationship.getTarget().getThirdLevelEntityRelationshipProperties()).isNotEmpty() + .allSatisfy(thirdLevel -> { + assertThat(thirdLevel.getOrder()).isGreaterThan(0); + assertThat(thirdLevel.getTarget()).isNotNull(); + assertThat(thirdLevel.getTarget().getSomeValue()).isEqualTo(thirdLevelValue); + }); + }); } @Test @Tag("GH-2819") - void inheritanceAndProjectionShouldMapRelatedNodesCorrectly(@Autowired GH2819Repository repository, @Autowired Driver driver) { + void inheritanceAndProjectionShouldMapRelatedNodesCorrectly(@Autowired GH2819Repository repository, + @Autowired Driver driver) { try (var session = driver.session()) { - session.run("CREATE (a:ParentA:ChildA{name:'parentA', id:'a'})-[:HasBs]->(b:ParentB:ChildB{name:'parentB', id:'b'})-[:HasCs]->(c:ParentC:ChildC{name:'parentC', id:'c'})").consume(); + session.run( + "CREATE (a:ParentA:ChildA{name:'parentA', id:'a'})-[:HasBs]->(b:ParentB:ChildB{name:'parentB', id:'b'})-[:HasCs]->(c:ParentC:ChildC{name:'parentC', id:'c'})") + .consume(); } var childAProjection = repository.findById("a", GH2819Model.ChildAProjection.class); @@ -1238,8 +1366,8 @@ class IssuesIT extends TestBase { friendAndRelative.name = "friendAndRelative"; // root -> friend1 -> friendAndRelative - // \ /| - // ------------------- + // \ /| + // ------------------- friend1.friends = List.of(friendAndRelative); entity.relatives = List.of(friendAndRelative); entity.friends = List.of(friend1); @@ -1293,17 +1421,17 @@ class IssuesIT extends TestBase { var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); var from1 = BugFromV1.builder() - .name("F1") - .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) - .build(); + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); var from2 = BugFromV1.builder() - .name("F2") - .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) - .build(); + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); var from3 = BugFromV1.builder() - .name("F3") - .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) - .build(); + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); to1.relatedBugs = Set.of(from1, from2, from3); toRepositoryV1.save(to1); @@ -1313,31 +1441,33 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2905") - void saveSingleEntities(@Autowired FromRepositoryV1 fromRepositoryV1, @Autowired ToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + void saveSingleEntities(@Autowired FromRepositoryV1 fromRepositoryV1, @Autowired ToRepositoryV1 toRepositoryV1, + @Autowired Driver driver) { var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); to1.relatedBugs = new HashSet<>(); to1 = toRepositoryV1.save(to1); var from1 = BugFromV1.builder() - .name("F1") - .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) - .build(); - // This is the key to solve 2905 when you had the annotation previously, you must maintain both ends of the bidirectional relationship. + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + // This is the key to solve 2905 when you had the annotation previously, you must + // maintain both ends of the bidirectional relationship. // SDN does not do this for you. to1.relatedBugs.add(from1); from1 = fromRepositoryV1.save(from1); var from2 = BugFromV1.builder() - .name("F2") - .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) - .build(); + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); // See above to1.relatedBugs.add(from2); var from3 = BugFromV1.builder() - .name("F3") - .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) - .build(); + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); to1.relatedBugs.add(from3); // See above @@ -1346,19 +1476,6 @@ class IssuesIT extends TestBase { assertGH2905Graph(driver); } - private static void assertGH2905Graph(Driver driver) { - var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f").execute().records(); - assertThat(result) - .hasSize(1) - .element(0).satisfies(r -> { - assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); - assertThat(r.get("f")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .hasSize(3); - }); - } - @Test @Tag("GH-2906") void storeFromRootAggregateToLeaf(@Autowired ToRepository toRepository, @Autowired Driver driver) { @@ -1368,17 +1485,14 @@ class IssuesIT extends TestBase { var from2 = new BugFrom("F2", "F2<-T1", to1); var from3 = new BugFrom("F3", "F3<-T1", to1); - to1.relatedBugs = Set.of( - new OutgoingBugRelationship(from1.reli.comment, from1), + to1.relatedBugs = Set.of(new OutgoingBugRelationship(from1.reli.comment, from1), new OutgoingBugRelationship(from2.reli.comment, from2), - new OutgoingBugRelationship(from3.reli.comment, from3) - ); + new OutgoingBugRelationship(from3.reli.comment, from3)); toRepository.save(to1); assertGH2906Graph(driver); } - @Test @Tag("GH-2906") void storeFromRootAggregateToContainer(@Autowired ToRepository toRepository, @Autowired Driver driver) { @@ -1394,11 +1508,9 @@ class IssuesIT extends TestBase { var from2 = new BugFrom("F2", "F2<-T1", to1); var from3 = new BugFrom("F3", "F3<-T1", to1); - to1.relatedBugs = Set.of( - new OutgoingBugRelationship(from1.reli.comment, from1), + to1.relatedBugs = Set.of(new OutgoingBugRelationship(from1.reli.comment, from1), new OutgoingBugRelationship(from2.reli.comment, from2), - new OutgoingBugRelationship(from3.reli.comment, from3) - ); + new OutgoingBugRelationship(from3.reli.comment, from3)); toRepository.save(to1); assertGH2906Graph(driver); @@ -1406,7 +1518,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, + @Autowired Driver driver) { var to1 = new BugTarget("T1", "BUG"); to1 = toRepository.save(to1); @@ -1439,7 +1552,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, + @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -1467,7 +1581,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, + @Autowired ToRepository toRepository, @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -1494,7 +1609,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveTwoSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveTwoSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, + @Autowired ToRepository toRepository, @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -1517,7 +1633,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, + @Autowired ToRepository toRepository, @Autowired Driver driver) { var uuid = toRepository.save(new BugTarget("T1", "BUG")).uuid; @@ -1539,7 +1656,8 @@ class IssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, + @Autowired ToRepository toRepository, @Autowired Driver driver) { var to1 = new BugTarget("T1", "BUG"); to1 = toRepository.save(to1); @@ -1555,7 +1673,8 @@ class IssuesIT extends TestBase { assertGH2906Graph(driver, 2); } - private BugFrom saveGH2906Entity(BugFrom from, String uuid, FromRepository fromRepository, ToRepository toRepository) { + private BugFrom saveGH2906Entity(BugFrom from, String uuid, FromRepository fromRepository, + ToRepository toRepository) { var to = toRepository.findById(uuid).orElseThrow(); from.reli.target = to; @@ -1564,41 +1683,17 @@ class IssuesIT extends TestBase { return fromRepository.save(from); } - private static void assertGH2906Graph(Driver driver) { - assertGH2906Graph(driver, 3); - } - - private static void assertGH2906Graph(Driver driver, int cnt) { - - var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); - var expectedRelationships = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d<-T1", i)).toArray(String[]::new); - - var result = driver.executableQuery("MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r").execute().records(); - assertThat(result) - .hasSize(1) - .element(0).satisfies(r -> { - assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); - assertThat(r.get("f")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) - .containsExactlyInAnyOrder(expectedNodes); - assertThat(r.get("r")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .map(rel -> ((Relationship) rel).get("comment").asString()) - .containsExactlyInAnyOrder(expectedRelationships); - }); - } - @Test @Tag("GH-2918") - void loadCycleFreeWithInAndOutgoingRelationship(@Autowired ConditionRepository conditionRepository, @Autowired Driver driver) { + void loadCycleFreeWithInAndOutgoingRelationship(@Autowired ConditionRepository conditionRepository, + @Autowired Driver driver) { var conditionSaved = conditionRepository.save(new ConditionNode()); - // Condition has both an incoming and outgoing relationship typed CAUSES that will cause a duplicate key - // in the map projection for the relationships to load. The fix was to indicate the direction in the name + // Condition has both an incoming and outgoing relationship typed CAUSES that will + // cause a duplicate key + // in the map projection for the relationships to load. The fix was to indicate + // the direction in the name // used for projecting the relationship, too assertThatNoException().isThrownBy(() -> conditionRepository.findById(conditionSaved.uuid)); } @@ -1619,36 +1714,35 @@ class IssuesIT extends TestBase { }); var zeroTo9k = Distance.between(0, Metrics.KILOMETERS, 90000, Metrics.KILOMETERS); - GeoPage pagedNodes = repository.findAllByPlaceNear(Place.SFO.getValue(), zeroTo9k, Pageable.ofSize(1)); + GeoPage pagedNodes = repository.findAllByPlaceNear(Place.SFO.getValue(), zeroTo9k, + Pageable.ofSize(1)); assertThat(pagedNodes).hasSize(1); assertThat(pagedNodes.getAverageDistance().getValue()).isCloseTo(0, Percentage.withPercentage(1)); assertThat(pagedNodes.getContent().get(0).getContent().getName()).isEqualTo("SFO"); pagedNodes = repository.findAllByPlaceNear(Place.SFO.getValue(), zeroTo9k, pagedNodes.nextPageable()); assertThat(pagedNodes).hasSize(1); - assertThat(pagedNodes.getAverageDistance().getValue()).isCloseTo(distanceBetweenSFOAndNeo4jHQ, Percentage.withPercentage(1)); + assertThat(pagedNodes.getAverageDistance().getValue()).isCloseTo(distanceBetweenSFOAndNeo4jHQ, + Percentage.withPercentage(1)); assertThat(pagedNodes.getContent().get(0).getContent().getName()).isEqualTo("NEO4J_HQ"); var distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distance); - assertThat(nodes).hasSize(1) - .first() - .satisfies(neo4jFoundInTheNearDistance); + assertThat(nodes).hasSize(1).first().satisfies(neo4jFoundInTheNearDistance); nodes = repository.findAllByPlaceNear(Place.CLARION.getValue(), distance); assertThat(nodes).isEmpty(); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), - Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); - assertThat(nodes).hasSize(1).first() - .satisfies(neo4jFoundInTheNearDistance); + Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(nodes).hasSize(1).first().satisfies(neo4jFoundInTheNearDistance); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), - Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); + Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); assertThat(nodes).isEmpty(); - final Range distanceRange = Range.of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), - Range.Bound.unbounded()); + final Range distanceRange = Range + .of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), Range.Bound.unbounded()); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distanceRange); assertThat(nodes).hasSize(1).first().satisfies(gr -> { var d = gr.getDistance(); @@ -1680,15 +1774,18 @@ class IssuesIT extends TestBase { logbackCapture.addLogger("org.springframework.data.neo4j.cypher", Level.DEBUG); var nodes = repository.findAllByName("NEO4J_HQ", PageRequest.of(0, 10, Sort.by(Sort.Order.asc("name")))); assertThat(nodes).isNotEmpty(); - assertThat(logbackCapture.getFormattedMessages()).noneMatch(l -> l.contains("locatedNode.name, locatedNode.name")); - } finally { + assertThat(logbackCapture.getFormattedMessages()) + .noneMatch(l -> l.contains("locatedNode.name, locatedNode.name")); + } + finally { logbackCapture.resetLogLevel(); } } @Tag("GH-2963") @Test - void customQueriesShouldKeepWorkingWithoutSpecifyingTheRelDirectionInTheirQueries(@Autowired MyRepository myRepository) { + void customQueriesShouldKeepWorkingWithoutSpecifyingTheRelDirectionInTheirQueries( + @Autowired MyRepository myRepository) { // set up data in database MyModel myNestedModel = new MyModel(); myNestedModel.setName("nested"); @@ -1736,19 +1833,8 @@ class IssuesIT extends TestBase { d1.setTargetNode(new BaseNode()); d1.setD("d1"); - node.setRelationships(Map.of( - "a", List.of( - a1, a2, b2 - ), - "b", List.of( - b1, a3 - ) - )); - nodeFail.setRelationships(Map.of( - "c", List.of( - c1, d1 - ) - )); + node.setRelationships(Map.of("a", List.of(a1, a2, b2), "b", List.of(b1, a3))); + nodeFail.setRelationships(Map.of("c", List.of(c1, d1))); var persistedNode = gh2973Repository.save(node); var persistedNodeFail = gh2973Repository.save(nodeFail); @@ -1756,16 +1842,13 @@ class IssuesIT extends TestBase { var loadedNode = gh2973Repository.findById(persistedNode.getId()).get(); List relationshipsA = loadedNode.getRelationships().get("a"); List relationshipsB = loadedNode.getRelationships().get("b"); - assertThat(relationshipsA).satisfiesExactlyInAnyOrder( - r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), + assertThat(relationshipsA).satisfiesExactlyInAnyOrder(r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), r2 -> assertThat(r2).isOfAnyClassIn(RelationshipA.class), - r3 -> assertThat(r3).isOfAnyClassIn(RelationshipB.class) - ); - assertThat(relationshipsB).satisfiesExactlyInAnyOrder( - r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), - r2 -> assertThat(r2).isOfAnyClassIn(RelationshipB.class) - ); - // without type info, the relationships are all same type and not the base class BaseRelationship + r3 -> assertThat(r3).isOfAnyClassIn(RelationshipB.class)); + assertThat(relationshipsB).satisfiesExactlyInAnyOrder(r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), + r2 -> assertThat(r2).isOfAnyClassIn(RelationshipB.class)); + // without type info, the relationships are all same type and not the base class + // BaseRelationship var loadedNodeFail = gh2973Repository.findById(persistedNodeFail.getId()).get(); List relationshipsCFail = loadedNodeFail.getRelationships().get("c"); assertThat(relationshipsCFail.get(0)).isNotExactlyInstanceOf(BaseRelationship.class); @@ -1778,24 +1861,25 @@ class IssuesIT extends TestBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Bean - public UnrelatedObjectPropertyConverterAsBean converterBean() { + UnrelatedObjectPropertyConverterAsBean converterBean() { return new UnrelatedObjectPropertyConverterAsBean(); } @Override public PlatformTransactionManager transactionManager(Driver driver, - DatabaseSelectionProvider databaseNameProvider) { + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -1806,128 +1890,7 @@ class IssuesIT extends TestBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } - private static void assertWriteAndReadConversionForProperty( - Neo4jPersistentEntity entity, - String propertyName, - DomainObjectRepository repository, - Driver driver, - BookmarkCapture bookmarkCapture - ) { - Neo4jPersistentProperty property = entity.getPersistentProperty(propertyName); - PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(new DomainObject()); - - propertyAccessor.setProperty(property, new UnrelatedObject(true, 4711L)); - DomainObject domainObject = repository.save(propertyAccessor.getBean()); - - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - var node = session - .run("MATCH (n:DomainObject {id: $id}) RETURN n", - Collections.singletonMap("id", domainObject.getId())) - .single().get(0).asNode(); - assertThat(node.get(propertyName).asString()).isEqualTo("true;4711"); - } - - domainObject = repository.findById(domainObject.getId()).get(); - UnrelatedObject unrelatedObject = (UnrelatedObject) entity.getPropertyAccessor(domainObject) - .getProperty(property); - assertThat(unrelatedObject) - .satisfies(t -> { - assertThat(t.isABooleanValue()).isTrue(); - assertThat(t.getALongValue()).isEqualTo(4711L); - }); - } - - private static void assertAll(List entities) { - - assertThat(entities).hasSize(4); - assertThat(entities).allSatisfy(v -> { - switch (v.getName()) { - case "A" -> assertA(Optional.of(v)); - case "B" -> assertB(Optional.of(v)); - case "D" -> assertD(Optional.of(v)); - } - }); - } - - private static void assertA(Optional a) { - - assertThat(a).hasValueSatisfying(s -> { - assertThat(s.getName()).isEqualTo("A"); - assertThat(s.getSomeRelationsOut()) - .hasSize(1) - .first().satisfies(b -> { - assertThat(b.getSomeData()).isEqualTo("d1"); - assertThat(b.getTargetPerson().getName()).isEqualTo("B"); - assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty(); - }); - }); - } - - private static void assertD(Optional d) { - - assertThat(d).hasValueSatisfying(s -> { - assertThat(s.getName()).isEqualTo("D"); - assertThat(s.getSomeRelationsOut()) - .hasSize(1) - .first().satisfies(c -> { - assertThat(c.getSomeData()).isEqualTo("d3"); - assertThat(c.getTargetPerson().getName()).isEqualTo("C"); - assertThat(c.getTargetPerson().getSomeRelationsOut()) - .hasSize(1) - .first().satisfies(b -> { - assertThat(b.getSomeData()).isEqualTo("d2"); - assertThat(b.getTargetPerson().getName()).isEqualTo("B"); - assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty(); - }); - }); - }); - } - - private static void assertB(Optional b) { - - assertThat(b).hasValueSatisfying(s -> { - assertThat(s.getName()).isEqualTo("B"); - assertThat(s.getSomeRelationsOut()).isEmpty(); - }); - } - - private static void assertThatTestObjectHasBeenCreated(Driver driver, BookmarkCapture bookmarkCapture, - TestObject testObject) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Map arguments = new HashMap<>(); - arguments.put("id", testObject.getId()); - arguments.put("num", testObject.getData().getNum()); - arguments.put("string", testObject.getData().getString()); - long cnt = session.run( - "MATCH (n:TestObject) WHERE n.id = $id AND n.dataNum = $num AND n.dataString = $string RETURN count(n)", - arguments) - .single().get(0).asLong(); - assertThat(cnt).isOne(); - } - } - - private static EntitiesAndProjections.GH2533Entity createData(GH2533Repository repository) { - EntitiesAndProjections.GH2533Entity n1 = new EntitiesAndProjections.GH2533Entity(); - EntitiesAndProjections.GH2533Entity n2 = new EntitiesAndProjections.GH2533Entity(); - EntitiesAndProjections.GH2533Entity n3 = new EntitiesAndProjections.GH2533Entity(); - - EntitiesAndProjections.GH2533Relationship r1 = new EntitiesAndProjections.GH2533Relationship(); - EntitiesAndProjections.GH2533Relationship r2 = new EntitiesAndProjections.GH2533Relationship(); - - n1.name = "n1"; - n2.name = "n2"; - n3.name = "n3"; - - r1.target = n2; - r2.target = n3; - - n1.relationships = Collections.singletonMap("has_relationship_with", List.of(r1)); - n2.relationships = Collections.singletonMap("has_relationship_with", List.of(r2)); - - return repository.save(n1); - } - - } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java index d8b943cd5..f9dfad5f9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java @@ -15,38 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues; -import static org.assertj.core.api.Assertions.as; -import static org.assertj.core.api.Assertions.assertThat; - -import org.assertj.core.api.InstanceOfAssertFactories; -import org.assertj.core.api.ThrowingConsumer; -import org.assertj.core.data.Percentage; -import org.junit.jupiter.api.BeforeEach; -import org.neo4j.cypherdsl.core.Cypher; -import org.neo4j.cypherdsl.core.LabelExpression; -import org.neo4j.cypherdsl.core.Node; -import org.neo4j.driver.Value; -import org.neo4j.driver.types.Relationship; -import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.domain.Range; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.GeoResult; -import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.integration.issues.gh2905.BugFromV1; -import org.springframework.data.neo4j.integration.issues.gh2905.BugRelationshipV1; -import org.springframework.data.neo4j.integration.issues.gh2905.BugTargetV1; -import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveFromRepositoryV1; -import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveToRepositoryV1; -import org.springframework.data.neo4j.integration.issues.gh2906.BugFrom; -import org.springframework.data.neo4j.integration.issues.gh2906.BugTarget; -import org.springframework.data.neo4j.integration.issues.gh2906.BugTargetContainer; -import org.springframework.data.neo4j.integration.issues.gh2906.OutgoingBugRelationship; -import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveFromRepository; -import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveToRepository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -60,19 +28,37 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.IntStream; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ThrowingConsumer; +import org.assertj.core.data.Percentage; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.LabelExpression; +import org.neo4j.cypherdsl.core.Node; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; +import org.neo4j.driver.types.Relationship; +import org.neo4j.driver.types.TypeSystem; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Range; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -94,6 +80,17 @@ import org.springframework.data.neo4j.integration.issues.gh2533.EntitiesAndProje import org.springframework.data.neo4j.integration.issues.gh2533.ReactiveGH2533Repository; import org.springframework.data.neo4j.integration.issues.gh2572.GH2572Child; import org.springframework.data.neo4j.integration.issues.gh2572.ReactiveGH2572Repository; +import org.springframework.data.neo4j.integration.issues.gh2905.BugFromV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugRelationshipV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugTargetV1; +import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveFromRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveToRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2906.BugFrom; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTarget; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTargetContainer; +import org.springframework.data.neo4j.integration.issues.gh2906.OutgoingBugRelationship; +import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveFromRepository; +import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveToRepository; import org.springframework.data.neo4j.integration.issues.gh2908.LocatedNode; import org.springframework.data.neo4j.integration.issues.gh2908.Place; import org.springframework.data.neo4j.integration.issues.gh2908.ReactiveLocatedNodeRepository; @@ -104,9 +101,11 @@ import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Sodom - Sodom */ @Neo4jIntegrationTest @DisplayNameGeneration(SimpleDisplayNameGeneratorWithTags.class) @@ -129,9 +128,58 @@ class ReactiveIssuesIT extends TestBase { } } + private static void assertGH2905Graph(Driver driver) { + var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f") + .execute() + .records(); + assertThat(result).hasSize(1).element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .hasSize(3); + }); + } + + private static Consumer assertRelations() { + return e1 -> { + assertThat(e1.reli.id).isNotNull(); + assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + }; + } + + private static void assertGH2906Graph(Driver driver) { + assertGH2906Graph(driver, 3); + } + + private static void assertGH2906Graph(Driver driver, int cnt) { + + var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); + var expectedRelationships = IntStream.rangeClosed(1, cnt) + .mapToObj(i -> String.format("F%d<-T1", i)) + .toArray(String[]::new); + + var result = driver + .executableQuery( + "MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r") + .execute() + .records(); + assertThat(result).hasSize(1).element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) + .containsExactlyInAnyOrder(expectedNodes); + assertThat(r.get("r")).matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(rel -> ((Relationship) rel).get("comment").asString()) + .containsExactlyInAnyOrder(expectedRelationships); + }); + } + @BeforeEach void setup(@Autowired BookmarkCapture bookmarkCapture) { - List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); + List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", + "BugTargetBase", "BugTargetContainer"); var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); for (int i = 1; i < labelsToBeRemoved.size(); i++) { labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); @@ -156,53 +204,47 @@ class ReactiveIssuesIT extends TestBase { AtomicLong bId = new AtomicLong(); AtomicReference cRef = new AtomicReference<>(); skuRepo.save(new Sku(0L, "A")) - .zipWith(skuRepo.save(new Sku(1L, "B"))) - .zipWith(skuRepo.save(new Sku(2L, "C"))) - .zipWith(skuRepo.save(new Sku(3L, "D"))).flatMap(t -> { - Sku a = t.getT1().getT1().getT1(); - Sku b = t.getT1().getT1().getT2(); - Sku c = t.getT1().getT2(); - Sku d = t.getT2(); + .zipWith(skuRepo.save(new Sku(1L, "B"))) + .zipWith(skuRepo.save(new Sku(2L, "C"))) + .zipWith(skuRepo.save(new Sku(3L, "D"))) + .flatMap(t -> { + Sku a = t.getT1().getT1().getT1(); + Sku b = t.getT1().getT1().getT2(); + Sku c = t.getT1().getT2(); + Sku d = t.getT2(); - bId.set(b.getId()); - cRef.set(c); - a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); - a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); - a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); - return skuRepo.save(a); - }).as(StepVerifier::create) - .expectNextMatches(a -> { - aId.set(a.getId()); // side-effects for the win - return a.getRangeRelationsOut().size() == 3; - }) - .verifyComplete(); + bId.set(b.getId()); + cRef.set(c); + a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); + return skuRepo.save(a); + }) + .as(StepVerifier::create) + .expectNextMatches(a -> { + aId.set(a.getId()); // side-effects for the win + return a.getRangeRelationsOut().size() == 3; + }) + .verifyComplete(); - skuRepo.findById(bId.get()) - .doOnNext(b -> assertThat(b.getRangeRelationsIn()).hasSize(1)) - .flatMap(b -> { - b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); - return skuRepo.save(b); - }) - .as(StepVerifier::create) - .assertNext(b -> { - assertThat(b.getRangeRelationsIn()).hasSize(1); - assertThat(b.getRangeRelationsOut()).hasSize(1); - }) - .verifyComplete(); + skuRepo.findById(bId.get()).doOnNext(b -> assertThat(b.getRangeRelationsIn()).hasSize(1)).flatMap(b -> { + b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); + return skuRepo.save(b); + }).as(StepVerifier::create).assertNext(b -> { + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSize(1); + }).verifyComplete(); - skuRepo.findById(aId.get()) - .as(StepVerifier::create) - .assertNext(a -> { - assertThat(a.getRangeRelationsOut()).hasSize(3); - assertThat(a.getRangeRelationsOut()).allSatisfy(r -> { - int expectedSize = 1; - if ("C".equals(r.getTargetSku().getName())) { - expectedSize = 2; - } - assertThat(r.getTargetSku().getRangeRelationsIn()).hasSize(expectedSize); - }); - }) - .verifyComplete(); + skuRepo.findById(aId.get()).as(StepVerifier::create).assertNext(a -> { + assertThat(a.getRangeRelationsOut()).hasSize(3); + assertThat(a.getRangeRelationsOut()).allSatisfy(r -> { + int expectedSize = 1; + if ("C".equals(r.getTargetSku().getName())) { + expectedSize = 2; + } + assertThat(r.getTargetSku().getRangeRelationsIn()).hasSize(expectedSize); + }); + }).verifyComplete(); } @RepeatedTest(5) @@ -213,60 +255,58 @@ class ReactiveIssuesIT extends TestBase { AtomicLong bId = new AtomicLong(); AtomicReference cRef = new AtomicReference<>(); skuRepo.findOneByName("A") - .zipWith(skuRepo.findOneByName("B")) - .zipWith(skuRepo.findOneByName("C")) - .zipWith(skuRepo.findOneByName("D")) - .flatMap(t -> { - SkuRO a = t.getT1().getT1().getT1(); - SkuRO b = t.getT1().getT1().getT2(); - SkuRO c = t.getT1().getT2(); - SkuRO d = t.getT2(); + .zipWith(skuRepo.findOneByName("B")) + .zipWith(skuRepo.findOneByName("C")) + .zipWith(skuRepo.findOneByName("D")) + .flatMap(t -> { + SkuRO a = t.getT1().getT1().getT1(); + SkuRO b = t.getT1().getT1().getT2(); + SkuRO c = t.getT1().getT2(); + SkuRO d = t.getT2(); - bId.set(b.getId()); - cRef.set(c); - a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); - a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); - a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); + bId.set(b.getId()); + cRef.set(c); + a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); - a.setName("a new name"); + a.setName("a new name"); - return skuRepo.save(a); - }).as(StepVerifier::create) - .expectNextMatches(a -> a.getRangeRelationsOut().size() == 3 && "a new name".equals(a.getName())) - .verifyComplete(); + return skuRepo.save(a); + }) + .as(StepVerifier::create) + .expectNextMatches(a -> a.getRangeRelationsOut().size() == 3 && "a new name".equals(a.getName())) + .verifyComplete(); - skuRepo.findOneByName("a new name") - .as(StepVerifier::create) - .verifyComplete(); + skuRepo.findOneByName("a new name").as(StepVerifier::create).verifyComplete(); - skuRepo.findOneByName("B") - .doOnNext(b -> { - assertThat(b.getRangeRelationsIn()).hasSize(1); - assertThat(b.getRangeRelationsOut()).hasSizeLessThanOrEqualTo(1); - }) - .flatMap(b -> { - b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); - return skuRepo.save(b); - }) - .as(StepVerifier::create) - .expectNextMatches(b -> b.getRangeRelationsIn().size() == 1 && b.getRangeRelationsOut().size() == 1) - .verifyComplete(); + skuRepo.findOneByName("B").doOnNext(b -> { + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSizeLessThanOrEqualTo(1); + }).flatMap(b -> { + b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); + return skuRepo.save(b); + }) + .as(StepVerifier::create) + .expectNextMatches(b -> b.getRangeRelationsIn().size() == 1 && b.getRangeRelationsOut().size() == 1) + .verifyComplete(); } @Test @Tag("GH-2326") void saveShouldAddAllLabels(@Autowired ReactiveAnimalRepository animalRepository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { List ids = new ArrayList<>(); List animals = Arrays.asList(new AbstractLevel2.AbstractLevel3.Concrete1(), new AbstractLevel2.AbstractLevel3.Concrete2()); - Flux.fromIterable(animals).flatMap(animalRepository::save) - .map(BaseEntity::getId) - .as(StepVerifier::create) - .recordWith(() -> ids) - .expectNextCount(2) - .verifyComplete(); + Flux.fromIterable(animals) + .flatMap(animalRepository::save) + .map(BaseEntity::getId) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(2) + .verifyComplete(); assertLabels(bookmarkCapture, ids); } @@ -274,17 +314,17 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2326") void saveAllShouldAddAllLabels(@Autowired ReactiveAnimalRepository animalRepository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { List ids = new ArrayList<>(); List animals = Arrays.asList(new AbstractLevel2.AbstractLevel3.Concrete1(), new AbstractLevel2.AbstractLevel3.Concrete2()); animalRepository.saveAll(animals) - .map(BaseEntity::getId) - .as(StepVerifier::create) - .recordWith(() -> ids) - .expectNextCount(2) - .verifyComplete(); + .map(BaseEntity::getId) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(2) + .verifyComplete(); assertLabels(bookmarkCapture, ids); } @@ -294,22 +334,20 @@ class ReactiveIssuesIT extends TestBase { void queriesFromCustomLocationsShouldBeFound(@Autowired ReactiveEntity2328Repository someRepository) { someRepository.getSomeEntityViaNamedQuery() - .as(StepVerifier::create) - .expectNextMatches(TestBase::requirements) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(TestBase::requirements) + .verifyComplete(); } @Test @Tag("GH-2347") void entitiesWithAssignedIdsSavedInBatchMustBeIdentifiableWithTheirInternalIds( - @Autowired ReactiveApplicationRepository applicationRepository, - @Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture - ) { - applicationRepository - .saveAll(Collections.singletonList(createData())) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + @Autowired ReactiveApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { + applicationRepository.saveAll(Collections.singletonList(createData())) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertSingleApplicationNodeWithMultipleWorkflows(driver, bookmarkCapture); } @@ -317,30 +355,19 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2347") void entitiesWithAssignedIdsMustBeIdentifiableWithTheirInternalIds( - @Autowired ReactiveApplicationRepository applicationRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { - applicationRepository - .save(createData()) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + @Autowired ReactiveApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { + applicationRepository.save(createData()).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); assertSingleApplicationNodeWithMultipleWorkflows(driver, bookmarkCapture); } @Test @Tag("GH-2346") void relationshipsStartingAtEntitiesWithAssignedIdsShouldBeCreated( - @Autowired ReactiveApplicationRepository applicationRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired ReactiveApplicationRepository applicationRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { createData((applications, workflows) -> { - applicationRepository.saveAll(applications) - .as(StepVerifier::create) - .expectNextCount(2L) - .verifyComplete(); + applicationRepository.saveAll(applications).as(StepVerifier::create).expectNextCount(2L).verifyComplete(); assertMultipleApplicationsNodeWithASingleWorkflow(driver, bookmarkCapture); }); @@ -349,15 +376,10 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2346") void relationshipsStartingAtEntitiesWithAssignedIdsShouldBeCreatedOtherDirection( - @Autowired ReactiveWorkflowRepository workflowRepository, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture - ) { + @Autowired ReactiveWorkflowRepository workflowRepository, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { createData((applications, workflows) -> { - workflowRepository.saveAll(workflows) - .as(StepVerifier::create) - .expectNextCount(2L) - .verifyComplete(); + workflowRepository.saveAll(workflows).as(StepVerifier::create).expectNextCount(2L).verifyComplete(); assertMultipleApplicationsNodeWithASingleWorkflow(driver, bookmarkCapture); }); @@ -365,105 +387,103 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2498") - void shouldNotDeleteFreshlyCreatedRelationships(@Autowired Driver driver, @Autowired - ReactiveNeo4jTemplate template) { + void shouldNotDeleteFreshlyCreatedRelationships(@Autowired Driver driver, + @Autowired ReactiveNeo4jTemplate template) { Group group = new Group(); group.setName("test"); - template.findById(1L, Device.class) - .flatMap(d -> { - group.getDevices().add(d); - return template.save(group); - }) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + template.findById(1L, Device.class).flatMap(d -> { + group.getDevices().add(d); + return template.save(group); + }).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); try (Session session = driver.session()) { Map parameters = new HashMap<>(); parameters.put("name", group.getName()); parameters.put("deviceId", 1L); - long cnt = session.run( - "MATCH (g:Group {name: $name}) <-[:BELONGS_TO]- (d:Device {id: $deviceId}) RETURN count(*)", - parameters) - .single().get(0).asLong(); + long cnt = session + .run("MATCH (g:Group {name: $name}) <-[:BELONGS_TO]- (d:Device {id: $deviceId}) RETURN count(*)", + parameters) + .single() + .get(0) + .asLong(); assertThat(cnt).isOne(); } } @Test @Tag("GH-2533") - void projectionWorksForDynamicRelationshipsOnSave(@Autowired ReactiveGH2533Repository repository, @Autowired ReactiveNeo4jTemplate neo4jTemplate) { - createData(repository) - .flatMap(rootEntity -> repository.findByIdWithLevelOneLinks(rootEntity.id)) - .flatMap(rootEntity -> neo4jTemplate.saveAs(rootEntity, - EntitiesAndProjections.GH2533EntityNodeWithOneLevelLinks.class)) - .flatMap(rootEntity -> neo4jTemplate.findById(rootEntity.getId(), - EntitiesAndProjections.GH2533Entity.class)) - .as(StepVerifier::create) - .assertNext(entity -> { - assertThat(entity.relationships).isNotEmpty(); - assertThat(entity.relationships.get("has_relationship_with")).isNotEmpty(); - assertThat(entity.relationships.get("has_relationship_with").get(0).target).isNotNull(); - assertThat( - entity.relationships.get("has_relationship_with").get(0).target.relationships).isNotEmpty(); - }) - .verifyComplete(); + void projectionWorksForDynamicRelationshipsOnSave(@Autowired ReactiveGH2533Repository repository, + @Autowired ReactiveNeo4jTemplate neo4jTemplate) { + createData(repository).flatMap(rootEntity -> repository.findByIdWithLevelOneLinks(rootEntity.id)) + .flatMap(rootEntity -> neo4jTemplate.saveAs(rootEntity, + EntitiesAndProjections.GH2533EntityNodeWithOneLevelLinks.class)) + .flatMap( + rootEntity -> neo4jTemplate.findById(rootEntity.getId(), EntitiesAndProjections.GH2533Entity.class)) + .as(StepVerifier::create) + .assertNext(entity -> { + assertThat(entity.relationships).isNotEmpty(); + assertThat(entity.relationships.get("has_relationship_with")).isNotEmpty(); + assertThat(entity.relationships.get("has_relationship_with").get(0).target).isNotNull(); + assertThat(entity.relationships.get("has_relationship_with").get(0).target.relationships).isNotEmpty(); + }) + .verifyComplete(); } @Test @Tag("GH-2533") - void saveRelatedEntityWithRelationships(@Autowired ReactiveGH2533Repository repository, @Autowired ReactiveNeo4jTemplate neo4jTemplate) { - createData(repository) - .flatMap(rootEntity -> repository.findByIdWithLevelOneLinks(rootEntity.id)) - .flatMap(rootEntity -> neo4jTemplate.saveAs(rootEntity, - EntitiesAndProjections.GH2533EntityNodeWithOneLevelLinks.class)) - .flatMap(rootEntity -> neo4jTemplate.findById(rootEntity.getId(), - EntitiesAndProjections.GH2533Entity.class)) - .as(StepVerifier::create) - .assertNext(entity -> { - assertThat(entity.relationships.get("has_relationship_with").get(0).target.name).isEqualTo("n2"); - assertThat(entity.relationships.get("has_relationship_with").get(0).target.relationships.get( - "has_relationship_with").get(0).target.name).isEqualTo("n3"); - }) - .verifyComplete(); + void saveRelatedEntityWithRelationships(@Autowired ReactiveGH2533Repository repository, + @Autowired ReactiveNeo4jTemplate neo4jTemplate) { + createData(repository).flatMap(rootEntity -> repository.findByIdWithLevelOneLinks(rootEntity.id)) + .flatMap(rootEntity -> neo4jTemplate.saveAs(rootEntity, + EntitiesAndProjections.GH2533EntityNodeWithOneLevelLinks.class)) + .flatMap( + rootEntity -> neo4jTemplate.findById(rootEntity.getId(), EntitiesAndProjections.GH2533Entity.class)) + .as(StepVerifier::create) + .assertNext(entity -> { + assertThat(entity.relationships.get("has_relationship_with").get(0).target.name).isEqualTo("n2"); + assertThat(entity.relationships.get("has_relationship_with").get(0).target.relationships + .get("has_relationship_with") + .get(0).target.name).isEqualTo("n3"); + }) + .verifyComplete(); } @Test @Tag("GH-2572") void allShouldFetchCorrectNumberOfChildNodes(@Autowired ReactiveGH2572Repository reactiveGH2572Repository) { reactiveGH2572Repository.getDogsForPerson("GH2572Parent-2") - .as(StepVerifier::create) - .expectNextCount(2L) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); } @Test @Tag("GH-2572") void allShouldNotFailWithoutMatchingRootNodes(@Autowired ReactiveGH2572Repository reactiveGH2572Repository) { reactiveGH2572Repository.getDogsForPerson("GH2572Parent-1") - .as(StepVerifier::create) - .expectNextCount(0L) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextCount(0L) + .verifyComplete(); } @Test @Tag("GH-2572") void oneShouldFetchCorrectNumberOfChildNodes(@Autowired ReactiveGH2572Repository reactiveGH2572Repository) { reactiveGH2572Repository.findOneDogForPerson("GH2572Parent-2") - .map(GH2572Child::getName) - .as(StepVerifier::create) - .expectNext("a-pet") - .verifyComplete(); + .map(GH2572Child::getName) + .as(StepVerifier::create) + .expectNext("a-pet") + .verifyComplete(); } @Test @Tag("GH-2572") void oneShouldNotFailWithoutMatchingRootNodes(@Autowired ReactiveGH2572Repository reactiveGH2572Repository) { reactiveGH2572Repository.findOneDogForPerson("GH2572Parent-1") - .as(StepVerifier::create) - .expectNextCount(0L) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextCount(0L) + .verifyComplete(); } @Test @@ -472,17 +492,17 @@ class ReactiveIssuesIT extends TestBase { var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); var from1 = BugFromV1.builder() - .name("F1") - .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) - .build(); + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); var from2 = BugFromV1.builder() - .name("F2") - .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) - .build(); + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); var from3 = BugFromV1.builder() - .name("F3") - .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) - .build(); + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); to1.relatedBugs = Set.of(from1, from2, from3); toRepositoryV1.save(to1).then().as(StepVerifier::create).expectComplete().verify(); @@ -492,30 +512,32 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2905") - void saveSingleEntities(@Autowired ReactiveFromRepositoryV1 fromRepositoryV1, @Autowired ReactiveToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + void saveSingleEntities(@Autowired ReactiveFromRepositoryV1 fromRepositoryV1, + @Autowired ReactiveToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { var bugTargetV1 = BugTargetV1.builder().name("T1").type("BUG").build(); bugTargetV1.relatedBugs = new HashSet<>(); toRepositoryV1.save(bugTargetV1).flatMapMany(to1 -> { var from1 = BugFromV1.builder() - .name("F1") - .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) - .build(); - // This is the key to solve 2905 when you had the annotation previously, you must maintain both ends of the bidirectional relationship. + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + // This is the key to solve 2905 when you had the annotation previously, you + // must maintain both ends of the bidirectional relationship. // SDN does not do this for you. to1.relatedBugs.add(from1); var from2 = BugFromV1.builder() - .name("F2") - .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) - .build(); + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); // See above to1.relatedBugs.add(from2); var from3 = BugFromV1.builder() - .name("F3") - .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) - .build(); + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); to1.relatedBugs.add(from3); // See above @@ -525,19 +547,6 @@ class ReactiveIssuesIT extends TestBase { assertGH2905Graph(driver); } - private static void assertGH2905Graph(Driver driver) { - var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f").execute().records(); - assertThat(result) - .hasSize(1) - .element(0).satisfies(r -> { - assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); - assertThat(r.get("f")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .hasSize(3); - }); - } - @Test @Tag("GH-2906") void storeFromRootAggregateToLeaf(@Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { @@ -547,17 +556,14 @@ class ReactiveIssuesIT extends TestBase { var from2 = new BugFrom("F2", "F2<-T1", to1); var from3 = new BugFrom("F3", "F3<-T1", to1); - to1.relatedBugs = Set.of( - new OutgoingBugRelationship(from1.reli.comment, from1), + to1.relatedBugs = Set.of(new OutgoingBugRelationship(from1.reli.comment, from1), new OutgoingBugRelationship(from2.reli.comment, from2), - new OutgoingBugRelationship(from3.reli.comment, from3) - ); + new OutgoingBugRelationship(from3.reli.comment, from3)); toRepository.save(to1).as(StepVerifier::create).expectNextCount(1).verifyComplete(); assertGH2906Graph(driver); } - @Test @Tag("GH-2906") void storeFromRootAggregateToContainer(@Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { @@ -573,11 +579,9 @@ class ReactiveIssuesIT extends TestBase { var from2 = new BugFrom("F2", "F2<-T1", to1); var from3 = new BugFrom("F3", "F3<-T1", to1); - to1.relatedBugs = Set.of( - new OutgoingBugRelationship(from1.reli.comment, from1), + to1.relatedBugs = Set.of(new OutgoingBugRelationship(from1.reli.comment, from1), new OutgoingBugRelationship(from2.reli.comment, from2), - new OutgoingBugRelationship(from3.reli.comment, from3) - ); + new OutgoingBugRelationship(from3.reli.comment, from3)); toRepository.save(to1).as(StepVerifier::create).expectNextCount(1).verifyComplete(); assertGH2906Graph(driver); @@ -585,7 +589,8 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesToLeaf(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { var bt = new BugTarget("T1", "BUG"); toRepository.save(bt).flatMapMany(to1 -> { @@ -612,7 +617,8 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesToContainer(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -640,7 +646,8 @@ class ReactiveIssuesIT extends TestBase { @Test @Tag("GH-2906") - void saveSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -649,26 +656,24 @@ class ReactiveIssuesIT extends TestBase { to1.items.add(t1); to1.items.add(t2); - toRepository.save(to1) - .flatMapMany(x -> { - var uuid = x.uuid; - var from1 = new BugFrom("F1", "F1<-T1", null); - var from2 = new BugFrom("F2", "F2<-T1", null); - var from3 = new BugFrom("F3", "F3<-T1", null); + toRepository.save(to1).flatMapMany(x -> { + var uuid = x.uuid; + var from1 = new BugFrom("F1", "F1<-T1", null); + var from2 = new BugFrom("F2", "F2<-T1", null); + var from3 = new BugFrom("F3", "F3<-T1", null); - return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), saveGH2906Entity(from2, uuid, fromRepository, toRepository), saveGH2906Entity(from3, uuid, fromRepository, toRepository)); - }) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), + saveGH2906Entity(from2, uuid, fromRepository, toRepository), + saveGH2906Entity(from3, uuid, fromRepository, toRepository)); + }).then().as(StepVerifier::create).expectComplete().verify(); assertGH2906Graph(driver); } @Test @Tag("GH-2906") - void saveTwoSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveTwoSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { var t1 = new BugTarget("T1", "BUG"); var t2 = new BugTarget("T2", "BUG"); @@ -682,63 +687,60 @@ class ReactiveIssuesIT extends TestBase { var from1 = new BugFrom("F1", "F1<-T1", null); var from2 = new BugFrom("F2", "F2<-T1", null); - return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), saveGH2906Entity(from2, uuid, fromRepository, toRepository)); - }) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), + saveGH2906Entity(from2, uuid, fromRepository, toRepository)); + }).then().as(StepVerifier::create).expectComplete().verify(); assertGH2906Graph(driver, 2); } @Test @Tag("GH-2906") - void saveSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { toRepository.save(new BugTarget("T1", "BUG")) - .map(x -> x.uuid) - .flatMapMany(uuid -> Flux.concat( - saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository) - .doOnNext(assertRelations()), - saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) - .doOnNext(assertRelations()), - saveGH2906Entity(new BugFrom("F3", "F3<-T1", null), uuid, fromRepository, toRepository) - .doOnNext(assertRelations()) + .map(x -> x.uuid) + .flatMapMany(uuid -> Flux.concat( + saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()), + saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()), + saveGH2906Entity(new BugFrom("F3", "F3<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()) - )).then() - .as(StepVerifier::create) - .expectComplete().verify(); + )) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); assertGH2906Graph(driver); } - private static Consumer assertRelations() { - return e1 -> { - assertThat(e1.reli.id).isNotNull(); - assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); - }; - } - @Test @Tag("GH-2906") - void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, + @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { var to1 = new BugTarget("T1", "BUG"); toRepository.save(to1) - .map(x -> x.uuid) - .flatMapMany(uuid -> Flux.concat( - saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository), - saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) + .map(x -> x.uuid) + .flatMapMany(uuid -> Flux.concat( + saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository), + saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) - )).then() - .as(StepVerifier::create) - .expectComplete().verify(); + )) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); assertGH2906Graph(driver, 2); } - private Mono saveGH2906Entity(BugFrom from, String uuid, ReactiveFromRepository fromRepository, ReactiveToRepository toRepository) { + private Mono saveGH2906Entity(BugFrom from, String uuid, ReactiveFromRepository fromRepository, + ReactiveToRepository toRepository) { return toRepository.findById(uuid).flatMap(to -> { from.reli.target = to; @@ -748,33 +750,6 @@ class ReactiveIssuesIT extends TestBase { }); } - private static void assertGH2906Graph(Driver driver) { - assertGH2906Graph(driver, 3); - } - - private static void assertGH2906Graph(Driver driver, int cnt) { - - var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); - var expectedRelationships = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d<-T1", i)).toArray(String[]::new); - - var result = driver.executableQuery("MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r").execute().records(); - assertThat(result) - .hasSize(1) - .element(0).satisfies(r -> { - assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); - assertThat(r.get("f")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) - .containsExactlyInAnyOrder(expectedNodes); - assertThat(r.get("r")) - .matches(TypeSystem.getDefault().LIST()::isTypeOf) - .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) - .map(rel -> ((Relationship) rel).get("comment").asString()) - .containsExactlyInAnyOrder(expectedRelationships); - }); - } - @Test @Tag("GH-2908") void shouldSupportGeoResult(@Autowired ReactiveLocatedNodeRepository repository) { @@ -784,29 +759,34 @@ class ReactiveIssuesIT extends TestBase { assertThat(gr.getDistance().getValue()).isCloseTo(90 / 1000.0, Percentage.withPercentage(5)); }; - List> nodes = repository.findAllAsGeoResultsByPlaceNear(Place.SFO.getValue()).collectList().block(); + List> nodes = repository.findAllAsGeoResultsByPlaceNear(Place.SFO.getValue()) + .collectList() + .block(); assertThat(nodes).hasSize(2); var distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distance).collectList().block(); - assertThat(nodes).hasSize(1) - .first() - .satisfies(neo4jFoundInTheNearDistance); + assertThat(nodes).hasSize(1).first().satisfies(neo4jFoundInTheNearDistance); nodes = repository.findAllByPlaceNear(Place.CLARION.getValue(), distance).collectList().block(); assertThat(nodes).isEmpty(); - nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), - Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)).collectList().block(); - assertThat(nodes).hasSize(1).first() - .satisfies(neo4jFoundInTheNearDistance); + nodes = repository + .findAllByPlaceNear(Place.MINC.getValue(), + Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)) + .collectList() + .block(); + assertThat(nodes).hasSize(1).first().satisfies(neo4jFoundInTheNearDistance); - nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), - Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)).collectList().block(); + nodes = repository + .findAllByPlaceNear(Place.MINC.getValue(), + Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)) + .collectList() + .block(); assertThat(nodes).isEmpty(); - final Range distanceRange = Range.of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), - Range.Bound.unbounded()); + final Range distanceRange = Range + .of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), Range.Bound.unbounded()); nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distanceRange).collectList().block(); assertThat(nodes).hasSize(1).first().satisfies(gr -> { var d = gr.getDistance(); @@ -816,37 +796,6 @@ class ReactiveIssuesIT extends TestBase { }); } - @Configuration - @EnableTransactionManagement - @EnableReactiveNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - - return neo4jConnectionSupport.getDriver(); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, - ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, - Neo4jBookmarkManager.createReactive(bookmarkCapture)); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } - private Mono createData(ReactiveGH2533Repository repository) { EntitiesAndProjections.GH2533Entity n1 = new EntitiesAndProjections.GH2533Entity(); EntitiesAndProjections.GH2533Entity n2 = new EntitiesAndProjections.GH2533Entity(); @@ -868,4 +817,37 @@ class ReactiveIssuesIT extends TestBase { return repository.save(n1); } + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java b/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java index 98df773ed..758b1d4c3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/SimpleDisplayNameGeneratorWithTags.java @@ -27,12 +27,12 @@ import org.junit.platform.commons.util.AnnotationUtils; * Prepends the value of any tags defined for a given test method to the display name. * * @author Michael J. Simons - * @soundtrack Stranger Things: Music From The Netflix Original Series, Season 1 */ final class SimpleDisplayNameGeneratorWithTags extends DisplayNameGenerator.Simple { @Override - public String generateDisplayNameForMethod(List> enclosingInstanceTypes, Class testClass, Method testMethod) { + public String generateDisplayNameForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { var displayNameForMethod = testMethod.getName(); var tags = AnnotationUtils.findRepeatableAnnotations(testMethod, Tag.class); @@ -42,4 +42,5 @@ final class SimpleDisplayNameGeneratorWithTags extends DisplayNameGenerator.Simp return tags.stream().map(Tag::value).collect(Collectors.joining(", ", "", ": " + displayNameForMethod)); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/TestBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/TestBase.java index fc173fdc9..67b078f23 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/TestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/TestBase.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; @@ -33,6 +31,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.integration.issues.gh2328.Entity2328; import org.springframework.data.neo4j.integration.issues.gh2347.Application; @@ -41,91 +40,48 @@ import org.springframework.data.neo4j.integration.issues.gh2908.Place; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assertions.assertThat; + abstract class TestBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; protected static UUID idOfAnEntity2328; - @BeforeEach - protected final void beforeEach(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { - List labelsToDelete = List.of("AbstractBase", "AccountingMeasurementMeta", "Application", - "BaseNodeEntity", "CityModel", "ConcreteImplementationOne", "ConcreteImplementationTwo", - "Credential", "Device", - "DomainModel", "GH2533Entity", "Measurand", "MeasurementMeta", "SomethingInBetween", "SpecialKind", - "Vertex"); - - // Detach delete things - transaction.run(""" - MATCH (n) WHERE any(label IN labels(n) WHERE label in $labels ) - DETACH DELETE n - """, - Map.of("labels", labelsToDelete) - ).consume(); - transaction.run("MATCH ()- [r:KNOWS]-() DELETE r").consume(); - - // 2498 - transaction.run( - "UNWIND ['A', 'B', 'C'] AS name WITH name CREATE (n:DomainModel {id: randomUUID(), name: name})") - .consume(); - transaction.run("CREATE (n:Vertex {name: 'a'}) -[:CONNECTED_TO] ->(m:Vertex {name: 'b'})").consume(); - - // 2498/2500 - transaction.run("CREATE (d:Device {id: 1, name:'Testdevice', version:0})").consume(); - - // 2526 - transaction.run(""" - CREATE (o1:Measurand {measurandId: 'o1'}) - CREATE (acc1:AccountingMeasurementMeta:MeasurementMeta:BaseNodeEntity {nodeId: 'acc1'}) - CREATE (m1:MeasurementMeta:BaseNodeEntity {nodeId: 'm1'}) - CREATE (acc1)-[:USES{variable: 'A'}]->(m1) - CREATE (o1)-[:IS_MEASURED_BY{ manual: true }]->(acc1) - """ - ).consume(); - - // 2415 - transaction.run(""" - CREATE (root:NodeEntity:BaseNodeEntity{nodeId: 'root'}) - CREATE (company:NodeEntity:BaseNodeEntity{nodeId: 'comp'}) - CREATE (cred:Credential{id: 'uuid-1', name: 'Creds'}) - CREATE (company)-[:CHILD_OF]->(root) - CREATE (root)-[:HAS_CREDENTIAL]->(cred) - CREATE (company)-[:WITH_CREDENTIAL]->(cred) - """); - - transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - protected static void setupGH2289(QueryRunner queryRunner) { queryRunner.run("MATCH (s:SKU_RO) DETACH DELETE s").consume(); for (int i = 0; i < 4; ++i) { - queryRunner.run("CREATE (s:SKU_RO {number: $i, name: $n, `composite.a`: $a})", - Values.parameters("i", i, "n", new String(new char[]{(char) ('A' + i)}), "a", 10 - i)).consume(); + queryRunner + .run("CREATE (s:SKU_RO {number: $i, name: $n, `composite.a`: $a})", + Values.parameters("i", i, "n", new String(new char[] { (char) ('A' + i) }), "a", 10 - i)) + .consume(); } } protected static void setupGH2328(QueryRunner queryRunner) { - idOfAnEntity2328 = UUID.fromString( - queryRunner.run("CREATE (f:Entity2328 {name: 'A name', id: randomUUID()}) RETURN f.id").single() - .get(0).asString()); + idOfAnEntity2328 = UUID + .fromString(queryRunner.run("CREATE (f:Entity2328 {name: 'A name', id: randomUUID()}) RETURN f.id") + .single() + .get(0) + .asString()); } protected static void setupGH2572(QueryRunner queryRunner) { queryRunner.run("CREATE (p:GH2572Parent {id: 'GH2572Parent-1', name:'no-pets'})"); - queryRunner.run("CREATE (p:GH2572Parent {id: 'GH2572Parent-2', name:'one-pet'}) <-[:IS_PET]- (:GH2572Child {id: 'GH2572Child-3', name: 'a-pet'})"); - queryRunner.run("MATCH (p:GH2572Parent {id: 'GH2572Parent-2'}) CREATE (p) <-[:IS_PET]- (:GH2572Child {id: 'GH2572Child-4', name: 'another-pet'})"); + queryRunner.run( + "CREATE (p:GH2572Parent {id: 'GH2572Parent-2', name:'one-pet'}) <-[:IS_PET]- (:GH2572Child {id: 'GH2572Child-3', name: 'a-pet'})"); + queryRunner.run( + "MATCH (p:GH2572Parent {id: 'GH2572Parent-2'}) CREATE (p) <-[:IS_PET]- (:GH2572Child {id: 'GH2572Child-4', name: 'another-pet'})"); } protected static void setupGH2908(QueryRunner queryRunner) { EnumSet places = EnumSet.of(Place.NEO4J_HQ, Place.SFO); for (Place value : places) { - queryRunner.run("CREATE (l:LocatedNode {name: $name, place: $place})", Map.of("name", value.name(), "place", value.getValue())); - queryRunner.run("CREATE (l:LocatedNodeWithSelfRef {name: $name, place: $place})-[:NEXT]->(n:LocatedNodeWithSelfRef {name: $name + 'next'})", Map.of("name", value.name(), "place", value.getValue())); + queryRunner.run("CREATE (l:LocatedNode {name: $name, place: $place})", + Map.of("name", value.name(), "place", value.getValue())); + queryRunner.run( + "CREATE (l:LocatedNodeWithSelfRef {name: $name, place: $place})-[:NEXT]->(n:LocatedNodeWithSelfRef {name: $name + 'next'})", + Map.of("name", value.name(), "place", value.getValue())); } } @@ -134,12 +90,12 @@ abstract class TestBase { for (String id : ids) { List labels = session.executeRead( tx -> tx.run("MATCH (n) WHERE n.id = $id RETURN labels(n)", Collections.singletonMap("id", id)) - .single().get(0).asList( - Value::asString)); - assertThat(labels) - .hasSize(3) - .contains("AbstractLevel2", "AbstractLevel3") - .containsAnyOf("Concrete1", "Concrete2"); + .single() + .get(0) + .asList(Value::asString)); + assertThat(labels).hasSize(3) + .contains("AbstractLevel2", "AbstractLevel3") + .containsAnyOf("Concrete1", "Concrete2"); } } @@ -181,31 +137,81 @@ abstract class TestBase { } protected static void assertSingleApplicationNodeWithMultipleWorkflows(Driver driver, - BookmarkCapture bookmarkCapture) { + BookmarkCapture bookmarkCapture) { try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Record record = session.executeRead( - tx -> tx.run("MATCH (a:Application)-->(w) RETURN a, collect(w) as workflows").single()); + Record record = session + .executeRead(tx -> tx.run("MATCH (a:Application)-->(w) RETURN a, collect(w) as workflows").single()); assertThat(record.get("a").asNode().get("id").asString()).isEqualTo("app-1"); - assertThat(record.get("workflows").asList(v -> v.asNode().get("id").asString())).containsExactlyInAnyOrder( - "wf-1", "wf-2"); + assertThat(record.get("workflows").asList(v -> v.asNode().get("id").asString())) + .containsExactlyInAnyOrder("wf-1", "wf-2"); } } protected static void assertMultipleApplicationsNodeWithASingleWorkflow(Driver driver, - BookmarkCapture bookmarkCapture) { + BookmarkCapture bookmarkCapture) { try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { List records = session.executeRead( tx -> tx.run("MATCH (a:Application)-->(w) RETURN a, collect(w) as workflows ORDER by a.id ASC") - .list()); + .list()); assertThat(records).hasSize(2); assertThat(records.get(0).get("a").asNode().get("id").asString()).isEqualTo("app-1"); - assertThat(records.get(0).get("workflows") - .asList(v -> v.asNode().get("id").asString())).containsExactlyInAnyOrder("wf-1"); + assertThat(records.get(0).get("workflows").asList(v -> v.asNode().get("id").asString())) + .containsExactlyInAnyOrder("wf-1"); assertThat(records.get(1).get("a").asNode().get("id").asString()).isEqualTo("app-2"); - assertThat(records.get(1).get("workflows") - .asList(v -> v.asNode().get("id").asString())).containsExactlyInAnyOrder("wf-2"); + assertThat(records.get(1).get("workflows").asList(v -> v.asNode().get("id").asString())) + .containsExactlyInAnyOrder("wf-2"); } } + + @BeforeEach + protected final void beforeEach(@Autowired BookmarkCapture bookmarkCapture) { + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + List labelsToDelete = List.of("AbstractBase", "AccountingMeasurementMeta", "Application", + "BaseNodeEntity", "CityModel", "ConcreteImplementationOne", "ConcreteImplementationTwo", + "Credential", "Device", "DomainModel", "GH2533Entity", "Measurand", "MeasurementMeta", + "SomethingInBetween", "SpecialKind", "Vertex"); + + // Detach delete things + transaction.run(""" + MATCH (n) WHERE any(label IN labels(n) WHERE label in $labels ) + DETACH DELETE n + """, Map.of("labels", labelsToDelete)).consume(); + transaction.run("MATCH ()- [r:KNOWS]-() DELETE r").consume(); + + // 2498 + transaction + .run("UNWIND ['A', 'B', 'C'] AS name WITH name CREATE (n:DomainModel {id: randomUUID(), name: name})") + .consume(); + transaction.run("CREATE (n:Vertex {name: 'a'}) -[:CONNECTED_TO] ->(m:Vertex {name: 'b'})").consume(); + + // 2498/2500 + transaction.run("CREATE (d:Device {id: 1, name:'Testdevice', version:0})").consume(); + + // 2526 + transaction.run(""" + CREATE (o1:Measurand {measurandId: 'o1'}) + CREATE (acc1:AccountingMeasurementMeta:MeasurementMeta:BaseNodeEntity {nodeId: 'acc1'}) + CREATE (m1:MeasurementMeta:BaseNodeEntity {nodeId: 'm1'}) + CREATE (acc1)-[:USES{variable: 'A'}]->(m1) + CREATE (o1)-[:IS_MEASURED_BY{ manual: true }]->(acc1) + """).consume(); + + // 2415 + transaction.run(""" + CREATE (root:NodeEntity:BaseNodeEntity{nodeId: 'root'}) + CREATE (company:NodeEntity:BaseNodeEntity{nodeId: 'comp'}) + CREATE (cred:Credential{id: 'uuid-1', name: 'Creds'}) + CREATE (company)-[:CHILD_OF]->(root) + CREATE (root)-[:HAS_CREDENTIAL]->(cred) + CREATE (company)-[:WITH_CREDENTIAL]->(cred) + """); + + transaction.commit(); + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/events/EventsPublisherIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/events/EventsPublisherIT.java index d6dbbc9dc..694749669 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/events/EventsPublisherIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/events/EventsPublisherIT.java @@ -15,10 +15,17 @@ */ package org.springframework.data.neo4j.integration.issues.events; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -41,11 +48,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -55,6 +57,8 @@ import static org.assertj.core.api.Assertions.assertThat; class EventsPublisherIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + static AtomicBoolean receivedBeforeCommitEvent = new AtomicBoolean(false); + static AtomicBoolean receivedAfterCommitEvent = new AtomicBoolean(false); @BeforeEach void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { @@ -65,12 +69,8 @@ class EventsPublisherIT { } } - static AtomicBoolean receivedBeforeCommitEvent = new AtomicBoolean(false); - - static AtomicBoolean receivedAfterCommitEvent = new AtomicBoolean(false); - @Test - // GH-2580 + // GH-2580 void beforeAndAfterCommitEventsShouldWork(@Autowired Neo4jObjectService service) { service.save("foobar"); @@ -78,6 +78,11 @@ class EventsPublisherIT { assertThat(receivedAfterCommitEvent).isTrue(); } + @Repository + interface Neo4jObjectRepository extends Neo4jRepository { + + } + @Component static class Neo4jObjectListener { @@ -88,17 +93,18 @@ class EventsPublisherIT { } @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) - public void onBeforeCommit(Neo4jMessage message) { - Optional optionalNeo4jObject = service.findById(message.getMessageId()); + void onBeforeCommit(Neo4jMessage message) { + Optional optionalNeo4jObject = this.service.findById(message.getMessageId()); receivedBeforeCommitEvent.compareAndSet(false, optionalNeo4jObject.isPresent()); } @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onAfterCommit(Neo4jMessage message) { - Optional optionalNeo4jObject = service.findById(message.getMessageId()); + void onAfterCommit(Neo4jMessage message) { + Optional optionalNeo4jObject = this.service.findById(message.getMessageId()); receivedAfterCommitEvent.compareAndSet(false, optionalNeo4jObject.isPresent()); } + } @Configuration @@ -108,13 +114,13 @@ class EventsPublisherIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager( - Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -127,6 +133,7 @@ class EventsPublisherIT { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -136,19 +143,26 @@ class EventsPublisherIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } static class Neo4jMessage { + private final String messageId; Neo4jMessage(String messageId) { this.messageId = messageId; } - public String getMessageId() { + String getMessageId() { return this.messageId; } + protected boolean canEqual(final Object other) { + return other instanceof Neo4jMessage; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -162,30 +176,23 @@ class EventsPublisherIT { } final Object this$messageId = this.getMessageId(); final Object other$messageId = other.getMessageId(); - if (this$messageId == null ? other$messageId != null : !this$messageId.equals(other$messageId)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof Neo4jMessage; + return Objects.equals(this$messageId, other$messageId); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $messageId = this.getMessageId(); - result = result * PRIME + ($messageId == null ? 43 : $messageId.hashCode()); + result = result * PRIME + (($messageId != null) ? $messageId.hashCode() : 43); return result; } + @Override public String toString() { return "EventsPublisherIT.Neo4jMessage(messageId=" + this.getMessageId() + ")"; } + } - @Repository - interface Neo4jObjectRepository extends Neo4jRepository { - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObject.java b/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObject.java index cdae2f438..8feecde20 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObject.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObject.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.events; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; @@ -44,6 +46,11 @@ public class Neo4jObject { this.id = id; } + protected boolean canEqual(final Object other) { + return other instanceof Neo4jObject; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -52,30 +59,26 @@ public class Neo4jObject { return false; } final Neo4jObject other = (Neo4jObject) o; - if (!other.canEqual((Object) this)) { + if (!other.canEqual(this)) { return false; } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof Neo4jObject; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); return result; } + @Override public String toString() { return "Neo4jObject(id=" + this.getId() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObjectService.java b/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObjectService.java index 3e3b214c0..26b88e3e9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObjectService.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/events/Neo4jObjectService.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.integration.issues.events; +import java.util.Optional; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - /** * @author Michael J. Simons */ @@ -29,20 +29,23 @@ import java.util.Optional; public class Neo4jObjectService { private final EventsPublisherIT.Neo4jObjectRepository neo4jObjectRepository; + private final ApplicationEventPublisher publisher; - public Neo4jObjectService(EventsPublisherIT.Neo4jObjectRepository neo4jObjectRepository, ApplicationEventPublisher publisher) { + public Neo4jObjectService(EventsPublisherIT.Neo4jObjectRepository neo4jObjectRepository, + ApplicationEventPublisher publisher) { this.neo4jObjectRepository = neo4jObjectRepository; this.publisher = publisher; } public Optional findById(String id) { - return neo4jObjectRepository.findById(id); + return this.neo4jObjectRepository.findById(id); } public Neo4jObject save(String id) { - Neo4jObject saved = neo4jObjectRepository.save(new Neo4jObject(id)); - publisher.publishEvent(new EventsPublisherIT.Neo4jMessage(id)); + Neo4jObject saved = this.neo4jObjectRepository.save(new Neo4jObject(id)); + this.publisher.publishEvent(new EventsPublisherIT.Neo4jMessage(id)); return saved; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObject.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObject.java index 1ed5109de..b49f0b264 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObject.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObject.java @@ -46,31 +46,32 @@ public class DomainObject { return this.id; } - public UnrelatedObject getStoredAsMultipleProperties() { - return this.storedAsMultipleProperties; - } - - public UnrelatedObject getStoredAsSingleProperty() { - return this.storedAsSingleProperty; - } - - public UnrelatedObject getStoredAsAnotherSingleProperty() { - return this.storedAsAnotherSingleProperty; - } - public void setId(String id) { this.id = id; } + public UnrelatedObject getStoredAsMultipleProperties() { + return this.storedAsMultipleProperties; + } + public void setStoredAsMultipleProperties(UnrelatedObject storedAsMultipleProperties) { this.storedAsMultipleProperties = storedAsMultipleProperties; } + public UnrelatedObject getStoredAsSingleProperty() { + return this.storedAsSingleProperty; + } + public void setStoredAsSingleProperty(UnrelatedObject storedAsSingleProperty) { this.storedAsSingleProperty = storedAsSingleProperty; } + public UnrelatedObject getStoredAsAnotherSingleProperty() { + return this.storedAsAnotherSingleProperty; + } + public void setStoredAsAnotherSingleProperty(UnrelatedObject storedAsAnotherSingleProperty) { this.storedAsAnotherSingleProperty = storedAsAnotherSingleProperty; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObjectRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObjectRepository.java index 9cf4636fe..8ca86a609 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObjectRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/DomainObjectRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface DomainObjectRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/GeneratedValueStrategy.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/GeneratedValueStrategy.java index 5e4d58c2e..6a50e56b0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/GeneratedValueStrategy.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/GeneratedValueStrategy.java @@ -26,7 +26,8 @@ public final class GeneratedValueStrategy implements IdGenerator { @Override public String generateId(String primaryLabel, Object entity) { - return "Please use something that one randomly create conflicting ids :) " + - ThreadLocalRandom.current().nextLong(); + return "Please use something that one randomly create conflicting ids :) " + + ThreadLocalRandom.current().nextLong(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObject.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObject.java index 44266fa45..60ac4adb8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObject.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObject.java @@ -22,10 +22,11 @@ package org.springframework.data.neo4j.integration.issues.gh2168; public class UnrelatedObject { private boolean aBooleanValue; + private Long aLongValue; public UnrelatedObject() { - aLongValue = 0L; + this.aLongValue = 0L; } public UnrelatedObject(boolean aBooleanValue, Long aLongValue) { @@ -37,15 +38,16 @@ public class UnrelatedObject { return this.aBooleanValue; } - public Long getALongValue() { - return this.aLongValue; - } - public void setABooleanValue(boolean aBooleanValue) { this.aBooleanValue = aBooleanValue; } + public Long getALongValue() { + return this.aLongValue; + } + public void setALongValue(Long aLongValue) { this.aLongValue = aLongValue; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectCompositePropertyConverter.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectCompositePropertyConverter.java index 6e8d22792..77aee45a6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectCompositePropertyConverter.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectCompositePropertyConverter.java @@ -21,15 +21,18 @@ import java.util.Optional; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; /** * @author Michael J. Simons */ -public final class UnrelatedObjectCompositePropertyConverter implements Neo4jPersistentPropertyToMapConverter { +public final class UnrelatedObjectCompositePropertyConverter + implements Neo4jPersistentPropertyToMapConverter { private static final String A_BOOLEAN_VALUE = "aBooleanValue"; + private static final String A_LONG_VALUE = "aLongValue"; @Override @@ -42,14 +45,11 @@ public final class UnrelatedObjectCompositePropertyConverter implements Neo4jPer @Override public UnrelatedObject compose(Map source, Neo4jConversionService neo4jConversionService) { - boolean aBooleanValue = Optional.ofNullable(source.get(A_BOOLEAN_VALUE)) - .map(Value::asBoolean) - .orElse(false); + boolean aBooleanValue = Optional.ofNullable(source.get(A_BOOLEAN_VALUE)).map(Value::asBoolean).orElse(false); - Long aLongValue = Optional.ofNullable(source.get(A_LONG_VALUE)) - .map(Value::asLong) - .orElse(0L); + Long aLongValue = Optional.ofNullable(source.get(A_LONG_VALUE)).map(Value::asLong).orElse(0L); return new UnrelatedObject(aBooleanValue, aLongValue); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverter.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverter.java index 55ca1d6d2..df9f36ae2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverter.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverter.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.integration.issues.gh2168; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; /** @@ -35,8 +36,10 @@ public final class UnrelatedObjectPropertyConverter implements Neo4jPersistentPr String[] concatenatedValues = source.asString().split(";"); if (concatenatedValues.length == 2) { - return new UnrelatedObject(Boolean.parseBoolean(concatenatedValues[0]), Long.parseLong(concatenatedValues[1])); + return new UnrelatedObject(Boolean.parseBoolean(concatenatedValues[0]), + Long.parseLong(concatenatedValues[1])); } return null; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java index c928a29a2..b245db108 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2168/UnrelatedObjectPropertyConverterAsBean.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.issues.gh2168; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.TypeSystem; + import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; /** @@ -45,4 +46,5 @@ public class UnrelatedObjectPropertyConverterAsBean implements Neo4jPersistentPr } return null; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeEntity.java index f1193c1e5..7c485f097 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeEntity.java @@ -24,7 +24,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; /** * @author Michael J. Simons - * @soundtrack Sodom - Sodom */ // tag::custom-query.paths.dm[] @Node @@ -37,23 +36,25 @@ public class SomeEntity { @Relationship(type = "SOME_RELATION_TO", direction = Relationship.Direction.OUTGOING) private Set someRelationsOut = new HashSet<>(); + // end::custom-query.paths.dm[] - public Long getNumber() { - return number; - } - - public String getName() { - return name; - } - - public Set getSomeRelationsOut() { - return someRelationsOut; - } - SomeEntity(Long number) { this.number = number; } + + public Long getNumber() { + return this.number; + } + + public String getName() { + return this.name; + } + + public Set getSomeRelationsOut() { + return this.someRelationsOut; + } // tag::custom-query.paths.dm[] + } // end::custom-query.paths.dm[] diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeRelation.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeRelation.java index d873536db..7fe81e8f8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeRelation.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/SomeRelation.java @@ -21,7 +21,6 @@ import org.springframework.data.neo4j.core.schema.TargetNode; /** * @author Michael J. Simons - * @soundtrack Sodom - Sodom */ // tag::custom-query.paths.dm[] @RelationshipProperties @@ -34,19 +33,21 @@ public class SomeRelation { @TargetNode private SomeEntity targetPerson; + // end::custom-query.paths.dm[] public Long getId() { - return id; + return this.id; } public String getSomeData() { - return someData; + return this.someData; } public SomeEntity getTargetPerson() { - return targetPerson; + return this.targetPerson; } // tag::custom-query.paths.dm[] + } // end::custom-query.paths.dm[] diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2244/Step.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2244/Step.java index d6f4890a5..52771f69f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2244/Step.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2244/Step.java @@ -30,7 +30,7 @@ public abstract class Step { private Long id; public Long getId() { - return id; + return this.id; } /** @@ -38,6 +38,7 @@ public abstract class Step { */ @Node public static class Chain extends Step { + } /** @@ -45,6 +46,7 @@ public abstract class Step { */ @Node public static class End extends Step { + } /** @@ -52,5 +54,7 @@ public abstract class Step { */ @Node public static class Origin extends Step { + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelation.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelation.java index 193ac8b4b..db0dc9acb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelation.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelation.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; @@ -25,13 +27,16 @@ import org.springframework.data.neo4j.core.schema.TargetNode; */ @RelationshipProperties public class RangeRelation { + @RelationshipId private Long id; @Property private double minDelta; + @Property private double maxDelta; + @Property private RelationType relationType; @@ -49,42 +54,47 @@ public class RangeRelation { return this.id; } - public double getMinDelta() { - return this.minDelta; - } - - public double getMaxDelta() { - return this.maxDelta; - } - - public RelationType getRelationType() { - return this.relationType; - } - - public Sku getTargetSku() { - return this.targetSku; - } - public void setId(Long id) { this.id = id; } + public double getMinDelta() { + return this.minDelta; + } + public void setMinDelta(double minDelta) { this.minDelta = minDelta; } + public double getMaxDelta() { + return this.maxDelta; + } + public void setMaxDelta(double maxDelta) { this.maxDelta = maxDelta; } + public RelationType getRelationType() { + return this.relationType; + } + public void setRelationType(RelationType relationType) { this.relationType = relationType; } + public Sku getTargetSku() { + return this.targetSku; + } + public void setTargetSku(Sku targetSku) { this.targetSku = targetSku; } + protected boolean canEqual(final Object other) { + return other instanceof RangeRelation; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -98,7 +108,7 @@ public class RangeRelation { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } if (Double.compare(this.getMinDelta(), other.getMinDelta()) != 0) { @@ -109,38 +119,36 @@ public class RangeRelation { } final Object this$relationType = this.getRelationType(); final Object other$relationType = other.getRelationType(); - if (this$relationType == null ? other$relationType != null : !this$relationType.equals(other$relationType)) { + if (!Objects.equals(this$relationType, other$relationType)) { return false; } final Object this$targetSku = this.getTargetSku(); final Object other$targetSku = other.getTargetSku(); - if (this$targetSku == null ? other$targetSku != null : !this$targetSku.equals(other$targetSku)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof RangeRelation; + return Objects.equals(this$targetSku, other$targetSku); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final long $minDelta = Double.doubleToLongBits(this.getMinDelta()); result = result * PRIME + (int) ($minDelta >>> 32 ^ $minDelta); final long $maxDelta = Double.doubleToLongBits(this.getMaxDelta()); result = result * PRIME + (int) ($maxDelta >>> 32 ^ $maxDelta); final Object $relationType = this.getRelationType(); - result = result * PRIME + ($relationType == null ? 43 : $relationType.hashCode()); + result = result * PRIME + (($relationType != null) ? $relationType.hashCode() : 43); final Object $targetSku = this.getTargetSku(); - result = result * PRIME + ($targetSku == null ? 43 : $targetSku.hashCode()); + result = result * PRIME + (($targetSku != null) ? $targetSku.hashCode() : 43); return result; } + @Override public String toString() { - return "RangeRelation(id=" + this.getId() + ", minDelta=" + this.getMinDelta() + ", maxDelta=" + this.getMaxDelta() + ", relationType=" + this.getRelationType() + ", targetSku=" + this.getTargetSku() + ")"; + return "RangeRelation(id=" + this.getId() + ", minDelta=" + this.getMinDelta() + ", maxDelta=" + + this.getMaxDelta() + ", relationType=" + this.getRelationType() + ", targetSku=" + this.getTargetSku() + + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java index c7dfd5538..f252a82f1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; @@ -31,8 +33,10 @@ public class RangeRelationRO { @Property private double minDelta; + @Property private double maxDelta; + @Property private RelationType relationType; @@ -50,42 +54,47 @@ public class RangeRelationRO { return this.id; } - public double getMinDelta() { - return this.minDelta; - } - - public double getMaxDelta() { - return this.maxDelta; - } - - public RelationType getRelationType() { - return this.relationType; - } - - public SkuRO getTargetSku() { - return this.targetSku; - } - public void setId(Long id) { this.id = id; } + public double getMinDelta() { + return this.minDelta; + } + public void setMinDelta(double minDelta) { this.minDelta = minDelta; } + public double getMaxDelta() { + return this.maxDelta; + } + public void setMaxDelta(double maxDelta) { this.maxDelta = maxDelta; } + public RelationType getRelationType() { + return this.relationType; + } + public void setRelationType(RelationType relationType) { this.relationType = relationType; } + public SkuRO getTargetSku() { + return this.targetSku; + } + public void setTargetSku(SkuRO targetSku) { this.targetSku = targetSku; } + protected boolean canEqual(final Object other) { + return other instanceof RangeRelationRO; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -99,7 +108,7 @@ public class RangeRelationRO { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } if (Double.compare(this.getMinDelta(), other.getMinDelta()) != 0) { @@ -110,38 +119,36 @@ public class RangeRelationRO { } final Object this$relationType = this.getRelationType(); final Object other$relationType = other.getRelationType(); - if (this$relationType == null ? other$relationType != null : !this$relationType.equals(other$relationType)) { + if (!Objects.equals(this$relationType, other$relationType)) { return false; } final Object this$targetSku = this.getTargetSku(); final Object other$targetSku = other.getTargetSku(); - if (this$targetSku == null ? other$targetSku != null : !this$targetSku.equals(other$targetSku)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof RangeRelationRO; + return Objects.equals(this$targetSku, other$targetSku); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final long $minDelta = Double.doubleToLongBits(this.getMinDelta()); result = result * PRIME + (int) ($minDelta >>> 32 ^ $minDelta); final long $maxDelta = Double.doubleToLongBits(this.getMaxDelta()); result = result * PRIME + (int) ($maxDelta >>> 32 ^ $maxDelta); final Object $relationType = this.getRelationType(); - result = result * PRIME + ($relationType == null ? 43 : $relationType.hashCode()); + result = result * PRIME + (($relationType != null) ? $relationType.hashCode() : 43); final Object $targetSku = this.getTargetSku(); - result = result * PRIME + ($targetSku == null ? 43 : $targetSku.hashCode()); + result = result * PRIME + (($targetSku != null) ? $targetSku.hashCode() : 43); return result; } + @Override public String toString() { - return "RangeRelationRO(id=" + this.getId() + ", minDelta=" + this.getMinDelta() + ", maxDelta=" + this.getMaxDelta() + ", relationType=" + this.getRelationType() + ", targetSku=" + this.getTargetSku() + ")"; + return "RangeRelationRO(id=" + this.getId() + ", minDelta=" + this.getMinDelta() + ", maxDelta=" + + this.getMaxDelta() + ", relationType=" + this.getRelationType() + ", targetSku=" + this.getTargetSku() + + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRORepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRORepository.java index d52371823..9b0dd2f9c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRORepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRORepository.java @@ -27,4 +27,5 @@ import org.springframework.stereotype.Repository; public interface ReactiveSkuRORepository extends ReactiveNeo4jRepository { Mono findOneByName(String name); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRepository.java index 75c476d41..288381b81 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveSkuRepository.java @@ -23,4 +23,5 @@ import org.springframework.stereotype.Repository; */ @Repository public interface ReactiveSkuRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RelationType.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RelationType.java index 252a77dc5..604bca05e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RelationType.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RelationType.java @@ -19,5 +19,7 @@ package org.springframework.data.neo4j.integration.issues.gh2289; * @author Michael J. Simons */ public enum RelationType { + MULTIPLICATIVE + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/Sku.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/Sku.java index f5453b623..c67fb9277 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/Sku.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/Sku.java @@ -15,15 +15,15 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; +import java.util.HashSet; +import java.util.Set; + 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; -import java.util.HashSet; -import java.util.Set; - /** * @author Michael J. Simons */ @@ -54,57 +54,54 @@ public class Sku { public RangeRelation rangeRelationTo(Sku sku, double minDelta, double maxDelta, RelationType relationType) { RangeRelation relationOut = new RangeRelation(sku, minDelta, maxDelta, relationType); RangeRelation relationIn = new RangeRelation(this, minDelta, maxDelta, relationType); - rangeRelationsOut.add(relationOut); + this.rangeRelationsOut.add(relationOut); sku.rangeRelationsIn.add(relationIn); return relationOut; } - @Override - public String toString() { - return "Sku{" + - "id=" + id + - ", number=" + number + - ", name='" + name + - '}'; - } - public Long getId() { return this.id; } - public Long getNumber() { - return this.number; - } - - public String getName() { - return this.name; - } - - public Set getRangeRelationsOut() { - return this.rangeRelationsOut; - } - - public Set getRangeRelationsIn() { - return this.rangeRelationsIn; - } - public void setId(Long id) { this.id = id; } + public Long getNumber() { + return this.number; + } + public void setNumber(Long number) { this.number = number; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public Set getRangeRelationsOut() { + return this.rangeRelationsOut; + } + public void setRangeRelationsOut(Set rangeRelationsOut) { this.rangeRelationsOut = rangeRelationsOut; } + public Set getRangeRelationsIn() { + return this.rangeRelationsIn; + } + public void setRangeRelationsIn(Set rangeRelationsIn) { this.rangeRelationsIn = rangeRelationsIn; } + + @Override + public String toString() { + return "Sku{" + "id=" + this.id + ", number=" + this.number + ", name='" + this.name + '}'; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java index 4cdfa4640..dde833bfa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java @@ -15,6 +15,11 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.neo4j.core.schema.CompositeProperty; import org.springframework.data.neo4j.core.schema.GeneratedValue; @@ -23,10 +28,6 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - /** * @author Michael J. Simons */ @@ -60,7 +61,7 @@ public class SkuRO { public RangeRelationRO rangeRelationTo(SkuRO sku, double minDelta, double maxDelta, RelationType relationType) { RangeRelationRO relationOut = new RangeRelationRO(sku, minDelta, maxDelta, relationType); - rangeRelationsOut.add(relationOut); + this.rangeRelationsOut.add(relationOut); return relationOut; } @@ -68,50 +69,55 @@ public class SkuRO { return this.id; } - public Long getNumber() { - return this.number; - } - - public String getName() { - return this.name; - } - - public Set getRangeRelationsOut() { - return this.rangeRelationsOut; - } - - public Set getRangeRelationsIn() { - return this.rangeRelationsIn; - } - public void setId(Long id) { this.id = id; } + public Long getNumber() { + return this.number; + } + public void setNumber(Long number) { this.number = number; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public Set getRangeRelationsOut() { + return this.rangeRelationsOut; + } + public void setRangeRelationsOut(Set rangeRelationsOut) { this.rangeRelationsOut = rangeRelationsOut; } + public Set getRangeRelationsIn() { + return this.rangeRelationsIn; + } + public void setRangeRelationsIn(Set rangeRelationsIn) { this.rangeRelationsIn = rangeRelationsIn; } public Map getComposite() { - return composite; + return this.composite; } public void setComposite(Map composite) { this.composite = composite; } + protected boolean canEqual(final Object other) { + return other instanceof SkuRO; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -125,35 +131,30 @@ public class SkuRO { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$number = this.getNumber(); final Object other$number = other.getNumber(); - if (this$number == null ? other$number != null : !this$number.equals(other$number)) { + if (!Objects.equals(this$number, other$number)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof SkuRO; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $number = this.getNumber(); - result = result * PRIME + ($number == null ? 43 : $number.hashCode()); + result = result * PRIME + (($number != null) ? $number.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); return result; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRORepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRORepository.java index fd0bdedae..496695666 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRORepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRORepository.java @@ -25,4 +25,5 @@ import org.springframework.stereotype.Repository; public interface SkuRORepository extends Neo4jRepository { SkuRO findOneByName(String name); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRepository.java index aaa50e5a3..2937d0240 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRepository.java @@ -23,4 +23,5 @@ import org.springframework.stereotype.Repository; */ @Repository public interface SkuRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Knows.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Knows.java index 12cbf8f13..35e49df40 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Knows.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Knows.java @@ -25,28 +25,29 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class Knows { - @RelationshipId - private Long id; - private final String description; @TargetNode private final Language language; + @RelationshipId + private Long id; + public Knows(String description, Language language) { this.description = description; this.language = language; } public Long getId() { - return id; + return this.id; } public String getDescription() { - return description; + return this.description; } public Language getLanguage() { - return language; + return this.language; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Language.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Language.java index 5ccb309b1..940787373 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Language.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Language.java @@ -32,6 +32,7 @@ public class Language { } public String getName() { - return name; + return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Person.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Person.java index e82b0fc18..1f8bdbae9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Person.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/Person.java @@ -29,12 +29,12 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class Person { + private final String name; + @Id @GeneratedValue(GeneratedValue.UUIDGenerator.class) private String id; - private final String name; - @Relationship("KNOWS") private List knownLanguages = new ArrayList<>(); @@ -46,18 +46,19 @@ public class Person { } public String getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public List getKnownLanguages() { - return knownLanguages; + return this.knownLanguages; } public void setKnownLanguages(List knownLanguages) { this.knownLanguages = knownLanguages; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonRepository.java index faf3605e0..5112950af 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonRepository.java @@ -36,8 +36,7 @@ public interface PersonRepository extends Neo4jRepository { MATCH (t:Language {name: rel.__target__.__id__}) CREATE (f)- [r:KNOWS {description: rel.__properties__.description}] -> (t) RETURN f, collect(r), collect(t) - """ - ) + """) Person updateRel(@Param("from") String from, @Param("relations") List relations); // Using the whole person object @@ -47,8 +46,7 @@ public interface PersonRepository extends Neo4jRepository { MATCH (t:Language {name: rel.__target__.__id__}) CREATE (f) - [r:KNOWS {description: rel.__properties__.description}] -> (t) RETURN f, collect(r), collect(t) - """ - ) + """) Person updateRel2(@Param("person") Person person); @Query(""" @@ -69,4 +67,5 @@ public interface PersonRepository extends Neo4jRepository { RETURN p """) Person queryWithMapOfRelationship(@Param("relationships") Map> relationshipList); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonService.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonService.java index 95f06d97b..cfeccde55 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonService.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2323/PersonService.java @@ -36,39 +36,42 @@ public class PersonService { public Person updateRel(String from, List languageNames) { - List knownLanguages = languageNames.stream().map(Language::new) - .map(language -> new Knows("Some description", language)) - .collect(Collectors.toList()); - return personRepository.updateRel(from, knownLanguages); + List knownLanguages = languageNames.stream() + .map(Language::new) + .map(language -> new Knows("Some description", language)) + .collect(Collectors.toList()); + return this.personRepository.updateRel(from, knownLanguages); } public Optional updateRel2(String id, List languageNames) { - Optional original = personRepository.findById(id); + Optional original = this.personRepository.findById(id); if (original.isPresent()) { Person person = original.get(); - List knownLanguages = languageNames.stream().map(Language::new) - .map(language -> new Knows("Some description", language)) - .collect(Collectors.toList()); + List knownLanguages = languageNames.stream() + .map(Language::new) + .map(language -> new Knows("Some description", language)) + .collect(Collectors.toList()); person.setKnownLanguages(knownLanguages); - return Optional.of(personRepository.updateRel2(person)); + return Optional.of(this.personRepository.updateRel2(person)); } return original; } public Optional updateRel3(String id) { - Optional original = personRepository.findById(id); + Optional original = this.personRepository.findById(id); if (original.isPresent()) { Person person = original.get(); person.setKnownLanguages(List.of(new Knows("Whatever", new Language("German")))); - return Optional.of(personRepository.updateRelWith11(person)); + return Optional.of(this.personRepository.updateRelWith11(person)); } return original; } public Person queryWithMapOfRelationship(String key, Knows knows) { - return personRepository.queryWithMapOfRelationship(Map.of(key, List.of(knows))); + return this.personRepository.queryWithMapOfRelationship(Map.of(key, List.of(knows))); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AbstractLevel2.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AbstractLevel2.java index eda322418..91e45218d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AbstractLevel2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AbstractLevel2.java @@ -27,13 +27,14 @@ public abstract class AbstractLevel2 extends BaseEntity { * Provides label `AbstractLevel3` */ @Node - public static abstract class AbstractLevel3 extends AbstractLevel2 { + public abstract static class AbstractLevel3 extends AbstractLevel2 { /** * Provides label `Concrete1` */ @Node public static class Concrete1 extends AbstractLevel3 { + } /** @@ -41,6 +42,9 @@ public abstract class AbstractLevel2 extends BaseEntity { */ @Node public static class Concrete2 extends AbstractLevel3 { + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AnimalRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AnimalRepository.java index 7249e17b2..b5550f6dd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AnimalRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/AnimalRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface AnimalRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/BaseEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/BaseEntity.java index 2403464d6..5cab9f29e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/BaseEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/BaseEntity.java @@ -29,6 +29,7 @@ public abstract class BaseEntity { private String id; public String getId() { - return id; + return this.id; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/ReactiveAnimalRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/ReactiveAnimalRepository.java index 7dfc4ab60..703a29329 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/ReactiveAnimalRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2326/ReactiveAnimalRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Michael J. Simons */ public interface ReactiveAnimalRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328.java index 008553736..36c6d07a0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328.java @@ -15,25 +15,24 @@ */ package org.springframework.data.neo4j.integration.issues.gh2328; +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 java.util.UUID; - /** * @author Michael J. Simons - * @soundtrack Motörhead - Better Motörhead Than Dead - Live At Hammersmith */ @Node public class Entity2328 { + private final String name; + @Id @GeneratedValue private UUID id; - private final String name; - public Entity2328(String name) { this.name = name; } @@ -45,4 +44,5 @@ public class Entity2328 { public String getName() { return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328Repository.java index 0a05cb547..3599cdd3e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/Entity2328Repository.java @@ -26,7 +26,9 @@ public interface Entity2328Repository extends Neo4jRepository // Without a custom query, repository creation would fail with // Could not create query for - // public abstract org.springframework.data.neo4j.integration.issues.gh2328.SomeEntity org.springframework.data.neo4j.integration.issues.gh2328.GH2328IT$SomeRepository.getSomeEntityViaNamedQuery()! + // public abstract org.springframework.data.neo4j.integration.issues.gh2328.SomeEntity + // org.springframework.data.neo4j.integration.issues.gh2328.GH2328IT$SomeRepository.getSomeEntityViaNamedQuery()! // Reason: No property getSomeEntityViaNamedQuery found for type SomeEntity!; Entity2328 getSomeEntityViaNamedQuery(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/ReactiveEntity2328Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/ReactiveEntity2328Repository.java index 4124a9ae6..ed6adc406 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/ReactiveEntity2328Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2328/ReactiveEntity2328Repository.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2328; -import reactor.core.publisher.Mono; - import java.util.UUID; +import reactor.core.publisher.Mono; + import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; /** @@ -28,7 +28,9 @@ public interface ReactiveEntity2328Repository extends ReactiveNeo4jRepository getSomeEntityViaNamedQuery(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Application.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Application.java index 9758f846a..4ac79a3ab 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Application.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Application.java @@ -39,10 +39,11 @@ public class Application { } public String getId() { - return id; + return this.id; } public List getWorkflows() { - return workflows; + return this.workflows; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ApplicationRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ApplicationRepository.java index c2a591217..cfea68cde 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ApplicationRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ApplicationRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface ApplicationRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveApplicationRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveApplicationRepository.java index 06786bcab..2b74ca339 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveApplicationRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveApplicationRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Michael J. Simons */ public interface ReactiveApplicationRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveWorkflowRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveWorkflowRepository.java index b50695955..f8290b122 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveWorkflowRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/ReactiveWorkflowRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Michael J. Simons */ public interface ReactiveWorkflowRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Workflow.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Workflow.java index 4c7daf645..c9aeccb48 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Workflow.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/Workflow.java @@ -36,14 +36,15 @@ public class Workflow { } public String getId() { - return id; + return this.id; } public Application getApplication() { - return application; + return this.application; } public void setApplication(Application application) { this.application = application; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/WorkflowRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/WorkflowRepository.java index dc959c37a..40b46df5b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/WorkflowRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2347/WorkflowRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface WorkflowRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/BaseNodeEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/BaseNodeEntity.java index c36eaba01..20b6dc86e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/BaseNodeEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/BaseNodeEntity.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2415; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -45,27 +47,31 @@ public class BaseNodeEntity { return new BaseNodeEntityBuilderImpl(); } - @Override - public String toString() { - return getClass().getSimpleName() + " - " + getName() + " (" + getNodeId() + ")"; - } - public String getNodeId() { return this.nodeId; } - public String getName() { - return this.name; - } - private void setNodeId(String nodeId) { this.nodeId = nodeId; } + public String getName() { + return this.name; + } + private void setName(String name) { this.name = name; } + protected boolean canEqual(final Object other) { + return other instanceof BaseNodeEntity; + } + + public BaseNodeEntityBuilder toBuilder() { + return new BaseNodeEntityBuilderImpl().$fillValuesFrom(this); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -79,35 +85,33 @@ public class BaseNodeEntity { } final Object this$nodeId = this.getNodeId(); final Object other$nodeId = other.getNodeId(); - if (this$nodeId == null ? other$nodeId != null : !this$nodeId.equals(other$nodeId)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof BaseNodeEntity; + return Objects.equals(this$nodeId, other$nodeId); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $nodeId = this.getNodeId(); - result = result * PRIME + ($nodeId == null ? 43 : $nodeId.hashCode()); + result = result * PRIME + (($nodeId != null) ? $nodeId.hashCode() : 43); return result; } - public BaseNodeEntityBuilder toBuilder() { - return new BaseNodeEntityBuilderImpl().$fillValuesFrom(this); + @Override + public String toString() { + return getClass().getSimpleName() + " - " + getName() + " (" + getNodeId() + ")"; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class BaseNodeEntityBuilder> { + public abstract static class BaseNodeEntityBuilder> { + private String nodeId; + private String name; private static void $fillValuesFromInstanceIntoBuilder(BaseNodeEntity instance, BaseNodeEntityBuilder b) { @@ -134,21 +138,29 @@ public class BaseNodeEntity { public abstract C build(); + @Override public String toString() { return "BaseNodeEntity.BaseNodeEntityBuilder(nodeId=" + this.nodeId + ", name=" + this.name + ")"; } + } - private static final class BaseNodeEntityBuilderImpl extends BaseNodeEntityBuilder { + private static final class BaseNodeEntityBuilderImpl + extends BaseNodeEntityBuilder { + private BaseNodeEntityBuilderImpl() { } + @Override protected BaseNodeEntityBuilderImpl self() { return this; } + @Override public BaseNodeEntity build() { return new BaseNodeEntity(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/Credential.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/Credential.java index 4c41588da..2cb8ec24f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/Credential.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/Credential.java @@ -15,7 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2415; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonIgnore; + import org.springframework.data.annotation.Immutable; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -33,8 +36,7 @@ public final class Credential { @JsonIgnore @Id @GeneratedValue(UUIDStringGenerator.class) - private final - String id; + private final String id; private final String name; @@ -55,18 +57,19 @@ public final class Credential { return this.name; } - public String toString() { - return "Credential(id=" + this.getId() + ", name=" + this.getName() + ")"; - } - public Credential withId(String id) { - return this.id == id ? this : new Credential(id, this.name); + return Objects.equals(this.id, id) ? this : new Credential(id, this.name); } public Credential withName(String name) { - return this.name == name ? this : new Credential(this.id, name); + return Objects.equals(this.name, name) ? this : new Credential(this.id, name); } + public CredentialBuilder toBuilder() { + return new CredentialBuilder().id(this.id).name(this.name); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -77,29 +80,30 @@ public final class Credential { final Credential other = (Credential) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); return result; } - public CredentialBuilder toBuilder() { - return new CredentialBuilder().id(this.id).name(this.name); + @Override + public String toString() { + return "Credential(id=" + this.getId() + ", name=" + this.getName() + ")"; } /** * the builder */ public static class CredentialBuilder { + private String id; + private String name; CredentialBuilder() { @@ -120,8 +124,11 @@ public final class Credential { return new Credential(this.id, this.name); } + @Override public String toString() { return "Credential.CredentialBuilder(id=" + this.id + ", name=" + this.name + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeEntity.java index cb674e139..bcb2230aa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeEntity.java @@ -15,12 +15,13 @@ */ package org.springframework.data.neo4j.integration.issues.gh2415; +import java.util.Set; + import com.fasterxml.jackson.annotation.JsonIgnore; + import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.Set; - /** * @author Andreas Berger */ @@ -48,28 +49,35 @@ public class NodeEntity extends BaseNodeEntity implements NodeWithDefinedCredent return new NodeEntityBuilderImpl(); } - @Override - public String toString() { - return super.toString(); - } - public Set getChildren() { return this.children; } - public Set getDefinedCredentials() { - return this.definedCredentials; - } - @JsonIgnore private void setChildren(Set children) { this.children = children; } + @Override + public Set getDefinedCredentials() { + return this.definedCredentials; + } + private void setDefinedCredentials(Set definedCredentials) { this.definedCredentials = definedCredentials; } + @Override + protected boolean canEqual(final Object other) { + return other instanceof NodeEntity; + } + + @Override + public NodeEntityBuilder toBuilder() { + return new NodeEntityBuilderImpl().$fillValuesFrom(this); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -81,32 +89,31 @@ public class NodeEntity extends BaseNodeEntity implements NodeWithDefinedCredent if (!other.canEqual((Object) this)) { return false; } - if (!super.equals(o)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof NodeEntity; + return super.equals(o); } + @Override public int hashCode() { int result = super.hashCode(); return result; } - public NodeEntityBuilder toBuilder() { - return new NodeEntityBuilderImpl().$fillValuesFrom(this); + @Override + public String toString() { + return super.toString(); } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class NodeEntityBuilder> extends BaseNodeEntityBuilder { + public abstract static class NodeEntityBuilder> + extends BaseNodeEntityBuilder { + private Set children; + private Set definedCredentials; private static void $fillValuesFromInstanceIntoBuilder(NodeEntity instance, NodeEntityBuilder b) { @@ -125,31 +132,42 @@ public class NodeEntity extends BaseNodeEntity implements NodeWithDefinedCredent return self(); } + @Override protected B $fillValuesFrom(C instance) { super.$fillValuesFrom(instance); NodeEntityBuilder.$fillValuesFromInstanceIntoBuilder(instance, this); return self(); } + @Override protected abstract B self(); + @Override public abstract C build(); + @Override public String toString() { - return "NodeEntity.NodeEntityBuilder(super=" + super.toString() + ", children=" + this.children + ", definedCredentials=" + this.definedCredentials + ")"; + return "NodeEntity.NodeEntityBuilder(super=" + super.toString() + ", children=" + this.children + + ", definedCredentials=" + this.definedCredentials + ")"; } + } private static final class NodeEntityBuilderImpl extends NodeEntityBuilder { + private NodeEntityBuilderImpl() { } + @Override protected NodeEntityBuilderImpl self() { return this; } + @Override public NodeEntity build() { return new NodeEntity(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeWithDefinedCredentials.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeWithDefinedCredentials.java index a1bfae851..27d3f9aff 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeWithDefinedCredentials.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2415/NodeWithDefinedCredentials.java @@ -27,4 +27,5 @@ public interface NodeWithDefinedCredentials { String getName(); Set getDefinedCredentials(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetEntity.java index ac137bb20..f9fe9b220 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetEntity.java @@ -32,18 +32,20 @@ public class WidgetEntity { @GeneratedValue @Id private Long id; + private String code; + private String label; @CompositeProperty private Map additionalFields = new HashMap<>(); public Long getId() { - return id; + return this.id; } public String getCode() { - return code; + return this.code; } public void setCode(String code) { @@ -51,7 +53,7 @@ public class WidgetEntity { } public String getLabel() { - return label; + return this.label; } public void setLabel(String label) { @@ -59,10 +61,11 @@ public class WidgetEntity { } public Map getAdditionalFields() { - return additionalFields; + return this.additionalFields; } public void setAdditionalFields(Map additionalFields) { this.additionalFields = additionalFields; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetProjection.java index 3a2596431..54746e77b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetProjection.java @@ -27,4 +27,5 @@ public interface WidgetProjection { String getLabel(); Map getAdditionalFields(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetRepository.java index ce979ac15..24c0438da 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2451/WidgetRepository.java @@ -25,4 +25,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; public interface WidgetRepository extends Neo4jRepository { Optional findByCode(String code); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Animal.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Animal.java index ce903b362..6582f0ab5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Animal.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Animal.java @@ -25,6 +25,8 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Animal") public abstract class Animal { + @Id private String uuid; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Boy.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Boy.java index f6cb8a1e2..fb02128af 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Boy.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Boy.java @@ -24,4 +24,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Boy") public class Boy extends PetOwner { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Cat.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Cat.java index 0b8941154..e8ef90225 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Cat.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Cat.java @@ -24,4 +24,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Cat") public class Cat extends Animal { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Dog.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Dog.java index 305fc36fb..3da3a2b18 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Dog.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Dog.java @@ -24,4 +24,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Dog") public class Dog extends Animal { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Girl.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Girl.java index 211a28b9c..193c07b88 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Girl.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/Girl.java @@ -24,4 +24,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Girl") public class Girl extends PetOwner { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwner.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwner.java index 177f3b20a..9b5f0b585 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwner.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwner.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.integration.issues.gh2459; +import java.util.List; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.List; - /** * Labels are written out on purpose for the test. * @@ -28,8 +28,10 @@ import java.util.List; */ @Node("PetOwner") public abstract class PetOwner { + @Id private String uuid; + @Relationship(type = "hasPet") private List pets; @@ -37,15 +39,16 @@ public abstract class PetOwner { return this.uuid; } - public List getPets() { - return this.pets; - } - public void setUuid(String uuid) { this.uuid = uuid; } + public List getPets() { + return this.pets; + } + public void setPets(List pets) { this.pets = pets; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwnerRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwnerRepository.java index 5f874cbf9..511d9234d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwnerRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2459/PetOwnerRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface PetOwnerRepository extends Neo4jRepository { + } 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 index 241a0d372..6b8ceab73 100644 --- 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 @@ -15,6 +15,12 @@ */ package org.springframework.data.neo4j.integration.issues.gh2474; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + import org.springframework.data.neo4j.core.schema.CompositeProperty; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -22,27 +28,23 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; - /** * @author Stephen Jackson */ @Node public class CityModel { + @Id @GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class) private UUID cityId; - @Relationship(value = "MAYOR") + @Relationship("MAYOR") private PersonModel mayor; - @Relationship(value = "CITIZEN") + @Relationship("CITIZEN") private List citizens = new ArrayList<>(); - @Relationship(value = "EMPLOYEE") + @Relationship("EMPLOYEE") private List cityEmployees = new ArrayList<>(); private String name; @@ -60,58 +62,63 @@ public class CityModel { return this.cityId; } - public PersonModel getMayor() { - return this.mayor; - } - - public List getCitizens() { - return this.citizens; - } - - public List getCityEmployees() { - return this.cityEmployees; - } - - public String getName() { - return this.name; - } - - public String getExoticProperty() { - return this.exoticProperty; - } - public void setCityId(UUID cityId) { this.cityId = cityId; } + public PersonModel getMayor() { + return this.mayor; + } + public void setMayor(PersonModel mayor) { this.mayor = mayor; } + public List getCitizens() { + return this.citizens; + } + public void setCitizens(List citizens) { this.citizens = citizens; } + public List getCityEmployees() { + return this.cityEmployees; + } + public void setCityEmployees(List cityEmployees) { this.cityEmployees = cityEmployees; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public String getExoticProperty() { + return this.exoticProperty; + } + public void setExoticProperty(String exoticProperty) { this.exoticProperty = exoticProperty; } public Map getCompositeProperty() { - return compositeProperty; + return this.compositeProperty; } public void setCompositeProperty(Map compositeProperty) { this.compositeProperty = compositeProperty; } + protected boolean canEqual(final Object other) { + return other instanceof CityModel; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -125,60 +132,58 @@ public class CityModel { } final Object this$cityId = this.getCityId(); final Object other$cityId = other.getCityId(); - if (this$cityId == null ? other$cityId != null : !this$cityId.equals(other$cityId)) { + if (!Objects.equals(this$cityId, other$cityId)) { return false; } final Object this$mayor = this.getMayor(); final Object other$mayor = other.getMayor(); - if (this$mayor == null ? other$mayor != null : !this$mayor.equals(other$mayor)) { + if (!Objects.equals(this$mayor, other$mayor)) { return false; } final Object this$citizens = this.getCitizens(); final Object other$citizens = other.getCitizens(); - if (this$citizens == null ? other$citizens != null : !this$citizens.equals(other$citizens)) { + if (!Objects.equals(this$citizens, other$citizens)) { return false; } final Object this$cityEmployees = this.getCityEmployees(); final Object other$cityEmployees = other.getCityEmployees(); - if (this$cityEmployees == null ? other$cityEmployees != null : !this$cityEmployees.equals(other$cityEmployees)) { + if (!Objects.equals(this$cityEmployees, other$cityEmployees)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$exoticProperty = this.getExoticProperty(); final Object other$exoticProperty = other.getExoticProperty(); - if (this$exoticProperty == null ? other$exoticProperty != null : !this$exoticProperty.equals(other$exoticProperty)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof CityModel; + return Objects.equals(this$exoticProperty, other$exoticProperty); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $cityId = this.getCityId(); - result = result * PRIME + ($cityId == null ? 43 : $cityId.hashCode()); + result = result * PRIME + (($cityId != null) ? $cityId.hashCode() : 43); final Object $mayor = this.getMayor(); - result = result * PRIME + ($mayor == null ? 43 : $mayor.hashCode()); + result = result * PRIME + (($mayor != null) ? $mayor.hashCode() : 43); final Object $citizens = this.getCitizens(); - result = result * PRIME + ($citizens == null ? 43 : $citizens.hashCode()); + result = result * PRIME + (($citizens != null) ? $citizens.hashCode() : 43); final Object $cityEmployees = this.getCityEmployees(); - result = result * PRIME + ($cityEmployees == null ? 43 : $cityEmployees.hashCode()); + result = result * PRIME + (($cityEmployees != null) ? $cityEmployees.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $exoticProperty = this.getExoticProperty(); - result = result * PRIME + ($exoticProperty == null ? 43 : $exoticProperty.hashCode()); + result = result * PRIME + (($exoticProperty != null) ? $exoticProperty.hashCode() : 43); return result; } + @Override public String toString() { - return "CityModel(cityId=" + this.getCityId() + ", mayor=" + this.getMayor() + ", citizens=" + this.getCitizens() + ", cityEmployees=" + this.getCityEmployees() + ", name=" + this.getName() + ", exoticProperty=" + this.getExoticProperty() + ")"; + return "CityModel(cityId=" + this.getCityId() + ", mayor=" + this.getMayor() + ", citizens=" + + this.getCitizens() + ", cityEmployees=" + this.getCityEmployees() + ", name=" + this.getName() + + ", exoticProperty=" + this.getExoticProperty() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java index 778ad5534..e4a3dabeb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java @@ -17,20 +17,26 @@ package org.springframework.data.neo4j.integration.issues.gh2474; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; /** * @author Stephen Jackson */ public class CityModelDTO { - private UUID cityId; - private String name; - private String exoticProperty; public PersonModelDTO mayor; + public List citizens = new ArrayList<>(); + public List cityEmployees = new ArrayList<>(); + private UUID cityId; + + private String name; + + private String exoticProperty; + public CityModelDTO() { } @@ -38,50 +44,55 @@ public class CityModelDTO { return this.cityId; } - public String getName() { - return this.name; - } - - public String getExoticProperty() { - return this.exoticProperty; - } - - public PersonModelDTO getMayor() { - return this.mayor; - } - - public List getCitizens() { - return this.citizens; - } - - public List getCityEmployees() { - return this.cityEmployees; - } - public void setCityId(UUID cityId) { this.cityId = cityId; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public String getExoticProperty() { + return this.exoticProperty; + } + public void setExoticProperty(String exoticProperty) { this.exoticProperty = exoticProperty; } + public PersonModelDTO getMayor() { + return this.mayor; + } + public void setMayor(PersonModelDTO mayor) { this.mayor = mayor; } + public List getCitizens() { + return this.citizens; + } + public void setCitizens(List citizens) { this.citizens = citizens; } + public List getCityEmployees() { + return this.cityEmployees; + } + public void setCityEmployees(List cityEmployees) { this.cityEmployees = cityEmployees; } + protected boolean canEqual(final Object other) { + return other instanceof CityModelDTO; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -95,67 +106,65 @@ public class CityModelDTO { } final Object this$cityId = this.getCityId(); final Object other$cityId = other.getCityId(); - if (this$cityId == null ? other$cityId != null : !this$cityId.equals(other$cityId)) { + if (!Objects.equals(this$cityId, other$cityId)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$exoticProperty = this.getExoticProperty(); final Object other$exoticProperty = other.getExoticProperty(); - if (this$exoticProperty == null ? other$exoticProperty != null : !this$exoticProperty.equals(other$exoticProperty)) { + if (!Objects.equals(this$exoticProperty, other$exoticProperty)) { return false; } final Object this$mayor = this.getMayor(); final Object other$mayor = other.getMayor(); - if (this$mayor == null ? other$mayor != null : !this$mayor.equals(other$mayor)) { + if (!Objects.equals(this$mayor, other$mayor)) { return false; } final Object this$citizens = this.getCitizens(); final Object other$citizens = other.getCitizens(); - if (this$citizens == null ? other$citizens != null : !this$citizens.equals(other$citizens)) { + if (!Objects.equals(this$citizens, other$citizens)) { return false; } final Object this$cityEmployees = this.getCityEmployees(); final Object other$cityEmployees = other.getCityEmployees(); - if (this$cityEmployees == null ? other$cityEmployees != null : !this$cityEmployees.equals(other$cityEmployees)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof CityModelDTO; + return Objects.equals(this$cityEmployees, other$cityEmployees); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $cityId = this.getCityId(); - result = result * PRIME + ($cityId == null ? 43 : $cityId.hashCode()); + result = result * PRIME + (($cityId != null) ? $cityId.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $exoticProperty = this.getExoticProperty(); - result = result * PRIME + ($exoticProperty == null ? 43 : $exoticProperty.hashCode()); + result = result * PRIME + (($exoticProperty != null) ? $exoticProperty.hashCode() : 43); final Object $mayor = this.getMayor(); - result = result * PRIME + ($mayor == null ? 43 : $mayor.hashCode()); + result = result * PRIME + (($mayor != null) ? $mayor.hashCode() : 43); final Object $citizens = this.getCitizens(); - result = result * PRIME + ($citizens == null ? 43 : $citizens.hashCode()); + result = result * PRIME + (($citizens != null) ? $citizens.hashCode() : 43); final Object $cityEmployees = this.getCityEmployees(); - result = result * PRIME + ($cityEmployees == null ? 43 : $cityEmployees.hashCode()); + result = result * PRIME + (($cityEmployees != null) ? $cityEmployees.hashCode() : 43); return result; } + @Override public String toString() { - return "CityModelDTO(cityId=" + this.getCityId() + ", name=" + this.getName() + ", exoticProperty=" + this.getExoticProperty() + ", mayor=" + this.getMayor() + ", citizens=" + this.getCitizens() + ", cityEmployees=" + this.getCityEmployees() + ")"; + return "CityModelDTO(cityId=" + this.getCityId() + ", name=" + this.getName() + ", exoticProperty=" + + this.getExoticProperty() + ", mayor=" + this.getMayor() + ", citizens=" + this.getCitizens() + + ", cityEmployees=" + this.getCityEmployees() + ")"; } /** * Nested projection */ public static class PersonModelDTO { + private UUID personId; public PersonModelDTO() { @@ -169,6 +178,11 @@ public class CityModelDTO { this.personId = personId; } + protected boolean canEqual(final Object other) { + return other instanceof PersonModelDTO; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -182,33 +196,30 @@ public class CityModelDTO { } final Object this$personId = this.getPersonId(); final Object other$personId = other.getPersonId(); - if (this$personId == null ? other$personId != null : !this$personId.equals(other$personId)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof PersonModelDTO; + return Objects.equals(this$personId, other$personId); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $personId = this.getPersonId(); - result = result * PRIME + ($personId == null ? 43 : $personId.hashCode()); + result = result * PRIME + (($personId != null) ? $personId.hashCode() : 43); return result; } + @Override public String toString() { return "CityModelDTO.PersonModelDTO(personId=" + this.getPersonId() + ")"; } + } /** * Nested projection */ public static class JobRelationshipDTO { + private PersonModelDTO person; public JobRelationshipDTO() { @@ -222,6 +233,11 @@ public class CityModelDTO { this.person = person; } + protected boolean canEqual(final Object other) { + return other instanceof JobRelationshipDTO; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -235,26 +251,23 @@ public class CityModelDTO { } final Object this$person = this.getPerson(); final Object other$person = other.getPerson(); - if (this$person == null ? other$person != null : !this$person.equals(other$person)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof JobRelationshipDTO; + return Objects.equals(this$person, other$person); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $person = this.getPerson(); - result = result * PRIME + ($person == null ? 43 : $person.hashCode()); + result = result * PRIME + (($person != null) ? $person.hashCode() : 43); return result; } + @Override public String toString() { return "CityModelDTO.JobRelationshipDTO(person=" + this.getPerson() + ")"; } + } + } 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 index d96468710..efa7d3ce4 100644 --- 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 @@ -31,10 +31,9 @@ public interface CityModelRepository extends Neo4jRepository { Optional findByCityId(UUID cityId); - @Query("" - + "MATCH (n:CityModel)" - + "RETURN n :#{orderBy(#sort)}") + @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/JobRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/JobRelationship.java index f370512d8..c075fe241 100644 --- 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2474; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.RelationshipProperties; @@ -25,6 +27,7 @@ import org.springframework.data.neo4j.core.schema.TargetNode; */ @RelationshipProperties public class JobRelationship { + @Id @GeneratedValue private Long id; @@ -41,26 +44,31 @@ public class JobRelationship { return this.id; } - public PersonModel getPerson() { - return this.person; - } - - public String getJobTitle() { - return this.jobTitle; - } - public void setId(Long id) { this.id = id; } + public PersonModel getPerson() { + return this.person; + } + public void setPerson(PersonModel person) { this.person = person; } + public String getJobTitle() { + return this.jobTitle; + } + public void setJobTitle(String jobTitle) { this.jobTitle = jobTitle; } + protected boolean canEqual(final Object other) { + return other instanceof JobRelationship; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -74,39 +82,36 @@ public class JobRelationship { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$person = this.getPerson(); final Object other$person = other.getPerson(); - if (this$person == null ? other$person != null : !this$person.equals(other$person)) { + if (!Objects.equals(this$person, other$person)) { return false; } final Object this$jobTitle = this.getJobTitle(); final Object other$jobTitle = other.getJobTitle(); - if (this$jobTitle == null ? other$jobTitle != null : !this$jobTitle.equals(other$jobTitle)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof JobRelationship; + return Objects.equals(this$jobTitle, other$jobTitle); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $person = this.getPerson(); - result = result * PRIME + ($person == null ? 43 : $person.hashCode()); + result = result * PRIME + (($person != null) ? $person.hashCode() : 43); final Object $jobTitle = this.getJobTitle(); - result = result * PRIME + ($jobTitle == null ? 43 : $jobTitle.hashCode()); + result = result * PRIME + (($jobTitle != null) ? $jobTitle.hashCode() : 43); return result; } + @Override public String toString() { - return "JobRelationship(id=" + this.getId() + ", person=" + this.getPerson() + ", jobTitle=" + this.getJobTitle() + ")"; + return "JobRelationship(id=" + this.getId() + ", person=" + this.getPerson() + ", jobTitle=" + + this.getJobTitle() + ")"; } + } 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 index 6ed4f84b4..dcb93ba61 100644 --- 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 @@ -15,23 +15,27 @@ */ package org.springframework.data.neo4j.integration.issues.gh2474; +import java.util.Objects; +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 java.util.UUID; - /** * @author Stephen Jackson */ @Node public class PersonModel { + @Id @GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class) private UUID personId; private String address; + private String name; + private String favoriteFood; public PersonModel() { @@ -41,34 +45,39 @@ public class PersonModel { return this.personId; } - public String getAddress() { - return this.address; - } - - public String getName() { - return this.name; - } - - public String getFavoriteFood() { - return this.favoriteFood; - } - public void setPersonId(UUID personId) { this.personId = personId; } + public String getAddress() { + return this.address; + } + public void setAddress(String address) { this.address = address; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public String getFavoriteFood() { + return this.favoriteFood; + } + public void setFavoriteFood(String favoriteFood) { this.favoriteFood = favoriteFood; } + protected boolean canEqual(final Object other) { + return other instanceof PersonModel; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -77,51 +86,48 @@ public class PersonModel { return false; } final PersonModel other = (PersonModel) o; - if (!other.canEqual((Object) this)) { + if (!other.canEqual(this)) { return false; } final Object this$personId = this.getPersonId(); final Object other$personId = other.getPersonId(); - if (this$personId == null ? other$personId != null : !this$personId.equals(other$personId)) { + if (!Objects.equals(this$personId, other$personId)) { return false; } final Object this$address = this.getAddress(); final Object other$address = other.getAddress(); - if (this$address == null ? other$address != null : !this$address.equals(other$address)) { + if (!Objects.equals(this$address, other$address)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$favoriteFood = this.getFavoriteFood(); final Object other$favoriteFood = other.getFavoriteFood(); - if (this$favoriteFood == null ? other$favoriteFood != null : !this$favoriteFood.equals(other$favoriteFood)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof PersonModel; + return Objects.equals(this$favoriteFood, other$favoriteFood); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $personId = this.getPersonId(); - result = result * PRIME + ($personId == null ? 43 : $personId.hashCode()); + result = result * PRIME + (($personId != null) ? $personId.hashCode() : 43); final Object $address = this.getAddress(); - result = result * PRIME + ($address == null ? 43 : $address.hashCode()); + result = result * PRIME + (($address != null) ? $address.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $favoriteFood = this.getFavoriteFood(); - result = result * PRIME + ($favoriteFood == null ? 43 : $favoriteFood.hashCode()); + result = result * PRIME + (($favoriteFood != null) ? $favoriteFood.hashCode() : 43); return result; } + @Override public String toString() { - return "PersonModel(personId=" + this.getPersonId() + ", address=" + this.getAddress() + ", name=" + this.getName() + ", favoriteFood=" + this.getFavoriteFood() + ")"; + return "PersonModel(personId=" + this.getPersonId() + ", address=" + this.getAddress() + ", name=" + + this.getName() + ", favoriteFood=" + this.getFavoriteFood() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java index c0d9329f0..b32af8c36 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java @@ -15,12 +15,13 @@ */ package org.springframework.data.neo4j.integration.issues.gh2474; -import org.springframework.data.neo4j.repository.Neo4jRepository; - import java.util.UUID; +import org.springframework.data.neo4j.repository.Neo4jRepository; + /** * @author Stephen Jackson */ public interface PersonModelRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2493/TestConverter.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2493/TestConverter.java index b0fb1b561..bd68a77f9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2493/TestConverter.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2493/TestConverter.java @@ -19,6 +19,7 @@ import java.util.Map; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; @@ -31,8 +32,7 @@ public class TestConverter implements Neo4jPersistentPropertyToMapConverter decompose(TestData property, - Neo4jConversionService neo4jConversionService) { + public Map decompose(TestData property, Neo4jConversionService neo4jConversionService) { if (property == null) { return Map.of(); @@ -42,8 +42,7 @@ public class TestConverter implements Neo4jPersistentPropertyToMapConverter source, - Neo4jConversionService neo4jConversionService) { + public TestData compose(Map source, Neo4jConversionService neo4jConversionService) { TestData data = new TestData(); if (source.get(NUM) != null) { data.setNum(source.get(NUM).asInt()); @@ -53,4 +52,5 @@ public class TestConverter implements Neo4jPersistentPropertyToMapConverter { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModel.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModel.java index 5654559bb..8c65113b3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModel.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModel.java @@ -27,21 +27,22 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class DomainModel { + private final String name; + @Id @GeneratedValue UUID id; - private final String name; - public DomainModel(String name) { this.name = name; } public UUID getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModelRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModelRepository.java index b38962d04..7a6921240 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModelRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/DomainModelRepository.java @@ -23,5 +23,7 @@ import org.springframework.data.neo4j.repository.support.CypherdslConditionExecu /** * @author Michael J. Simons */ -public interface DomainModelRepository extends Neo4jRepository, CypherdslConditionExecutor { +public interface DomainModelRepository + extends Neo4jRepository, CypherdslConditionExecutor { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Edge.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Edge.java index 32ffee613..217dd4606 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Edge.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Edge.java @@ -26,9 +26,11 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @SuppressWarnings("HiddenField") @RelationshipProperties public class Edge { + @Id @GeneratedValue Long id; + @TargetNode Vertex vertex; @@ -48,14 +50,14 @@ public class Edge { return this.id; } - public Vertex getVertex() { - return this.vertex; - } - public void setId(Long id) { this.id = id; } + public Vertex getVertex() { + return this.vertex; + } + public void setVertex(Vertex vertex) { this.vertex = vertex; } @@ -68,7 +70,9 @@ public class Edge { * the builder */ public static class EdgeBuilder { + private Long id; + private Vertex vertex; EdgeBuilder() { @@ -88,8 +92,11 @@ public class Edge { return new Edge(this.id, this.vertex); } + @Override public String toString() { return "Edge.EdgeBuilder(id=" + this.id + ", vertex=" + this.vertex + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Vertex.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Vertex.java index d2286e952..3f26cf796 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Vertex.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/Vertex.java @@ -15,23 +15,26 @@ */ package org.springframework.data.neo4j.integration.issues.gh2498; +import java.util.List; + 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.Relationship; -import java.util.List; - /** * @author Michael J. Simons */ @SuppressWarnings("HiddenField") @Node("Vertex") public class Vertex { + @Id @GeneratedValue Long id; + String name; + @Relationship(type = "CONNECTED_TO", direction = Relationship.Direction.INCOMING) List edges; @@ -52,22 +55,22 @@ public class Vertex { return this.id; } - public String getName() { - return this.name; - } - - public List getEdges() { - return this.edges; - } - public void setId(Long id) { this.id = id; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public List getEdges() { + return this.edges; + } + public void setEdges(List edges) { this.edges = edges; } @@ -80,8 +83,11 @@ public class Vertex { * the builder */ public static class VertexBuilder { + private Long id; + private String name; + private List edges; VertexBuilder() { @@ -106,8 +112,11 @@ public class Vertex { return new Vertex(this.id, this.name, this.edges); } + @Override public String toString() { return "Vertex.VertexBuilder(id=" + this.id + ", name=" + this.name + ", edges=" + this.edges + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/VertexRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/VertexRepository.java index 3aac5d9d8..c2a3cb8be 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/VertexRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2498/VertexRepository.java @@ -24,4 +24,5 @@ import org.springframework.stereotype.Repository; */ @Repository public interface VertexRepository extends Neo4jRepository, CypherdslConditionExecutor { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Device.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Device.java index 36c1f0b1e..ef5af7779 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Device.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Device.java @@ -15,14 +15,14 @@ */ package org.springframework.data.neo4j.integration.issues.gh2500; +import java.util.LinkedHashSet; +import java.util.Set; + import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.LinkedHashSet; -import java.util.Set; - /** * @author Michael J. Simons */ @@ -40,6 +40,38 @@ public class Device { @Relationship(type = "BELONGS_TO", direction = Relationship.Direction.OUTGOING) private Set groups = new LinkedHashSet<>(); + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return this.version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getGroups() { + return this.groups; + } + + public void setGroups(Set groups) { + this.groups = groups; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -51,45 +83,14 @@ public class Device { Device device = (Device) o; - return id.equals(device.id); + return this.id.equals(device.id); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; - result = 31 * result + (name != null ? name.hashCode() : 0); + int result = (this.id != null) ? this.id.hashCode() : 0; + result = 31 * result + ((this.name != null) ? this.name.hashCode() : 0); return result; } - public Long getId() { - return this.id; - } - - public Long getVersion() { - return this.version; - } - - public String getName() { - return this.name; - } - - public Set getGroups() { - return this.groups; - } - - public void setId(Long id) { - this.id = id; - } - - public void setVersion(Long version) { - this.version = version; - } - - public void setName(String name) { - this.name = name; - } - - public void setGroups(Set groups) { - this.groups = groups; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java index ead722e4b..a340db6ea 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2500/Group.java @@ -15,6 +15,9 @@ */ package org.springframework.data.neo4j.integration.issues.gh2500; +import java.util.LinkedHashSet; +import java.util.Set; + import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -22,9 +25,6 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.support.UUIDStringGenerator; -import java.util.LinkedHashSet; -import java.util.Set; - /** * @author Michael J. Simons */ @@ -46,6 +46,46 @@ public class Group { @Relationship(type = "GROUP_LINK") private Set groups = new LinkedHashSet<>(); + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getVersion() { + return this.version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getDevices() { + return this.devices; + } + + public void setDevices(Set devices) { + this.devices = devices; + } + + public Set getGroups() { + return this.groups; + } + + public void setGroups(Set groups) { + this.groups = groups; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -57,57 +97,18 @@ public class Group { Group group = (Group) o; - if (!id.equals(group.id)) { + if (!this.id.equals(group.id)) { return false; } - return name.equals(group.name); + return this.name.equals(group.name); } @Override public int hashCode() { int result = 7; - result = 31 * result + id.hashCode(); - result = 31 * result + name.hashCode(); + result = 31 * result + this.id.hashCode(); + result = 31 * result + this.name.hashCode(); return result; } - public String getId() { - return this.id; - } - - public Long getVersion() { - return this.version; - } - - public String getName() { - return this.name; - } - - public Set getDevices() { - return this.devices; - } - - public Set getGroups() { - return this.groups; - } - - public void setId(String id) { - this.id = id; - } - - public void setVersion(Long version) { - this.version = version; - } - - public void setName(String name) { - this.name = name; - } - - public void setDevices(Set devices) { - this.devices = devices; - } - - public void setGroups(Set groups) { - this.groups = groups; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java index d184c98d2..4194c69f2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java @@ -52,22 +52,29 @@ public class AccountingMeasurementMeta extends MeasurementMeta { return this.formula; } - public MeasurementMeta getBaseMeasurement() { - return this.baseMeasurement; - } - - public String toString() { - return "AccountingMeasurementMeta(formula=" + this.getFormula() + ", baseMeasurement=" + this.getBaseMeasurement() + ")"; - } - private void setFormula(String formula) { this.formula = formula; } + public MeasurementMeta getBaseMeasurement() { + return this.baseMeasurement; + } + private void setBaseMeasurement(MeasurementMeta baseMeasurement) { this.baseMeasurement = baseMeasurement; } + @Override + protected boolean canEqual(final Object other) { + return other instanceof AccountingMeasurementMeta; + } + + @Override + public AccountingMeasurementMetaBuilder toBuilder() { + return new AccountingMeasurementMetaBuilderImpl().$fillValuesFrom(this); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -79,35 +86,36 @@ public class AccountingMeasurementMeta extends MeasurementMeta { if (!other.canEqual((Object) this)) { return false; } - if (!super.equals(o)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof AccountingMeasurementMeta; + return super.equals(o); } + @Override public int hashCode() { int result = super.hashCode(); return result; } - public AccountingMeasurementMetaBuilder toBuilder() { - return new AccountingMeasurementMetaBuilderImpl().$fillValuesFrom(this); + @Override + public String toString() { + return "AccountingMeasurementMeta(formula=" + this.getFormula() + ", baseMeasurement=" + + this.getBaseMeasurement() + ")"; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class AccountingMeasurementMetaBuilder> extends MeasurementMetaBuilder { + public abstract static class AccountingMeasurementMetaBuilder> + extends MeasurementMetaBuilder { + private String formula; + private MeasurementMeta baseMeasurement; - private static void $fillValuesFromInstanceIntoBuilder(AccountingMeasurementMeta instance, AccountingMeasurementMetaBuilder b) { + private static void $fillValuesFromInstanceIntoBuilder(AccountingMeasurementMeta instance, + AccountingMeasurementMetaBuilder b) { b.formula(instance.formula); b.baseMeasurement(instance.baseMeasurement); } @@ -122,31 +130,43 @@ public class AccountingMeasurementMeta extends MeasurementMeta { return self(); } + @Override protected B $fillValuesFrom(C instance) { super.$fillValuesFrom(instance); AccountingMeasurementMetaBuilder.$fillValuesFromInstanceIntoBuilder(instance, this); return self(); } + @Override protected abstract B self(); + @Override public abstract C build(); + @Override public String toString() { - return "AccountingMeasurementMeta.AccountingMeasurementMetaBuilder(super=" + super.toString() + ", formula=" + this.formula + ", baseMeasurement=" + this.baseMeasurement + ")"; + return "AccountingMeasurementMeta.AccountingMeasurementMetaBuilder(super=" + super.toString() + ", formula=" + + this.formula + ", baseMeasurement=" + this.baseMeasurement + ")"; } + } - private static final class AccountingMeasurementMetaBuilderImpl extends AccountingMeasurementMetaBuilder { + private static final class AccountingMeasurementMetaBuilderImpl + extends AccountingMeasurementMetaBuilder { + private AccountingMeasurementMetaBuilderImpl() { } + @Override protected AccountingMeasurementMetaBuilderImpl self() { return this; } + @Override public AccountingMeasurementMeta build() { return new AccountingMeasurementMeta(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeFieldsProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeFieldsProjection.java index cbe81ec2e..7df88197b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeFieldsProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeFieldsProjection.java @@ -21,4 +21,5 @@ package org.springframework.data.neo4j.integration.issues.gh2526; public interface BaseNodeFieldsProjection { String getNodeId(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java index b808be8cd..344c7ca71 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java @@ -24,4 +24,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; public interface BaseNodeRepository extends Neo4jRepository { R findByNodeId(String nodeIds, Class clazz); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java index edf145e2a..9747e7949 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2526; +import java.util.Objects; + import org.springframework.data.annotation.Immutable; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; @@ -29,14 +31,12 @@ import org.springframework.data.neo4j.core.schema.TargetNode; public final class DataPoint { @RelationshipId - private final - Long id; + private final Long id; private final boolean manual; @TargetNode - private final - Measurand measurand; + private final Measurand measurand; public DataPoint(Long id, boolean manual, Measurand measurand) { this.id = id; @@ -56,22 +56,19 @@ public final class DataPoint { return this.measurand; } - public String toString() { - return "DataPoint(id=" + this.getId() + ", manual=" + this.isManual() + ", measurand=" + this.getMeasurand() + ")"; - } - public DataPoint withId(Long id) { - return this.id == id ? this : new DataPoint(id, this.manual, this.measurand); + return (Objects.equals(this.id, id)) ? this : new DataPoint(id, this.manual, this.measurand); } public DataPoint withManual(boolean manual) { - return this.manual == manual ? this : new DataPoint(this.id, manual, this.measurand); + return (this.manual != manual) ? new DataPoint(this.id, manual, this.measurand) : this; } public DataPoint withMeasurand(Measurand measurand) { - return this.measurand == measurand ? this : new DataPoint(this.id, this.manual, measurand); + return (this.measurand != measurand) ? new DataPoint(this.id, this.manual, measurand) : this; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -82,17 +79,22 @@ public final class DataPoint { final DataPoint other = (DataPoint) o; final Object this$measurand = this.getMeasurand(); final Object other$measurand = other.getMeasurand(); - if (this$measurand == null ? other$measurand != null : !this$measurand.equals(other$measurand)) { - return false; - } - return true; + return Objects.equals(this$measurand, other$measurand); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $measurand = this.getMeasurand(); - result = result * PRIME + ($measurand == null ? 43 : $measurand.hashCode()); + result = result * PRIME + (($measurand != null) ? $measurand.hashCode() : 43); return result; } + + @Override + public String toString() { + return "DataPoint(id=" + this.getId() + ", manual=" + this.isManual() + ", measurand=" + this.getMeasurand() + + ")"; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java index afe16aef0..2ba43342b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2526; +import java.util.Objects; + import org.springframework.data.annotation.Immutable; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -27,8 +29,7 @@ import org.springframework.data.neo4j.core.schema.Node; public final class Measurand { @Id - private final - String measurandId; + private final String measurandId; public Measurand(String measurandId) { this.measurandId = measurandId; @@ -38,6 +39,7 @@ public final class Measurand { return this.measurandId; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -48,21 +50,21 @@ public final class Measurand { final Measurand other = (Measurand) o; final Object this$measurandId = this.getMeasurandId(); final Object other$measurandId = other.getMeasurandId(); - if (this$measurandId == null ? other$measurandId != null : !this$measurandId.equals(other$measurandId)) { - return false; - } - return true; + return Objects.equals(this$measurandId, other$measurandId); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $measurandId = this.getMeasurandId(); - result = result * PRIME + ($measurandId == null ? 43 : $measurandId.hashCode()); + result = result * PRIME + (($measurandId != null) ? $measurandId.hashCode() : 43); return result; } + @Override public String toString() { return "Measurand(measurandId=" + this.getMeasurandId() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java index 06092ecea..a3b8212e1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.integration.issues.gh2526; +import java.util.Set; + import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.integration.issues.gh2415.BaseNodeEntity; -import java.util.Set; - /** * Defining relationship to measurand */ @@ -56,22 +56,29 @@ public class MeasurementMeta extends BaseNodeEntity { return this.dataPoints; } - public Set getVariables() { - return this.variables; - } - - public String toString() { - return "MeasurementMeta(dataPoints=" + this.getDataPoints() + ", variables=" + this.getVariables() + ")"; - } - private void setDataPoints(Set dataPoints) { this.dataPoints = dataPoints; } + public Set getVariables() { + return this.variables; + } + private void setVariables(Set variables) { this.variables = variables; } + @Override + protected boolean canEqual(final Object other) { + return other instanceof MeasurementMeta; + } + + @Override + public MeasurementMetaBuilder toBuilder() { + return new MeasurementMetaBuilderImpl().$fillValuesFrom(this); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -83,35 +90,35 @@ public class MeasurementMeta extends BaseNodeEntity { if (!other.canEqual((Object) this)) { return false; } - if (!super.equals(o)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof MeasurementMeta; + return super.equals(o); } + @Override public int hashCode() { int result = super.hashCode(); return result; } - public MeasurementMetaBuilder toBuilder() { - return new MeasurementMetaBuilderImpl().$fillValuesFrom(this); + @Override + public String toString() { + return "MeasurementMeta(dataPoints=" + this.getDataPoints() + ", variables=" + this.getVariables() + ")"; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class MeasurementMetaBuilder> extends BaseNodeEntityBuilder { + public abstract static class MeasurementMetaBuilder> + extends BaseNodeEntityBuilder { + private Set dataPoints; + private Set variables; - private static void $fillValuesFromInstanceIntoBuilder(MeasurementMeta instance, MeasurementMetaBuilder b) { + private static void $fillValuesFromInstanceIntoBuilder(MeasurementMeta instance, + MeasurementMetaBuilder b) { b.dataPoints(instance.dataPoints); b.variables(instance.variables); } @@ -126,31 +133,43 @@ public class MeasurementMeta extends BaseNodeEntity { return self(); } + @Override protected B $fillValuesFrom(C instance) { super.$fillValuesFrom(instance); MeasurementMetaBuilder.$fillValuesFromInstanceIntoBuilder(instance, this); return self(); } + @Override protected abstract B self(); + @Override public abstract C build(); + @Override public String toString() { - return "MeasurementMeta.MeasurementMetaBuilder(super=" + super.toString() + ", dataPoints=" + this.dataPoints + ", variables=" + this.variables + ")"; + return "MeasurementMeta.MeasurementMetaBuilder(super=" + super.toString() + ", dataPoints=" + + this.dataPoints + ", variables=" + this.variables + ")"; } + } - private static final class MeasurementMetaBuilderImpl extends MeasurementMetaBuilder { + private static final class MeasurementMetaBuilderImpl + extends MeasurementMetaBuilder { + private MeasurementMetaBuilderImpl() { } + @Override protected MeasurementMetaBuilderImpl self() { return this; } + @Override public MeasurementMeta build() { return new MeasurementMeta(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementProjection.java index 97898c078..1faac65a9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementProjection.java @@ -25,4 +25,5 @@ public interface MeasurementProjection extends BaseNodeFieldsProjection { Set getDataPoints(); Set getVariables(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java index b7174613a..f8835795d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2526; +import java.util.Objects; + import org.springframework.data.annotation.Immutable; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; @@ -27,13 +29,12 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties @Immutable public final class Variable { + @RelationshipId - private final - Long id; + private final Long id; @TargetNode - private final - MeasurementMeta measurement; + private final MeasurementMeta measurement; private final String variable; @@ -47,11 +48,6 @@ public final class Variable { return new Variable(null, measurement, variable); } - @Override - public String toString() { - return variable + ": " + measurement.getNodeId(); - } - public Long getId() { return this.id; } @@ -65,17 +61,18 @@ public final class Variable { } public Variable withId(Long id) { - return this.id == id ? this : new Variable(id, this.measurement, this.variable); + return Objects.equals(this.id, id) ? this : new Variable(id, this.measurement, this.variable); } public Variable withMeasurement(MeasurementMeta measurement) { - return this.measurement == measurement ? this : new Variable(this.id, measurement, this.variable); + return (this.measurement != measurement) ? new Variable(this.id, measurement, this.variable) : this; } public Variable withVariable(String variable) { - return this.variable == variable ? this : new Variable(this.id, this.measurement, variable); + return Objects.equals(this.variable, variable) ? this : new Variable(this.id, this.measurement, variable); } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -86,31 +83,35 @@ public final class Variable { final Variable other = (Variable) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$measurement = this.getMeasurement(); final Object other$measurement = other.getMeasurement(); - if (this$measurement == null ? other$measurement != null : !this$measurement.equals(other$measurement)) { + if (!Objects.equals(this$measurement, other$measurement)) { return false; } final Object this$variable = this.getVariable(); final Object other$variable = other.getVariable(); - if (this$variable == null ? other$variable != null : !this$variable.equals(other$variable)) { - return false; - } - return true; + return Objects.equals(this$variable, other$variable); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $measurement = this.getMeasurement(); - result = result * PRIME + ($measurement == null ? 43 : $measurement.hashCode()); + result = (result * PRIME) + (($measurement != null) ? $measurement.hashCode() : 43); final Object $variable = this.getVariable(); - result = result * PRIME + ($variable == null ? 43 : $variable.hashCode()); + result = (result * PRIME) + (($variable != null) ? $variable.hashCode() : 43); return result; } + + @Override + public String toString() { + return this.variable + ": " + this.measurement.getNodeId(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/VariableProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/VariableProjection.java index 01893ba3b..a56802b51 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/VariableProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/VariableProjection.java @@ -23,4 +23,5 @@ public interface VariableProjection { BaseNodeFieldsProjection getMeasurement(); String getVariable(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/InitialEntities.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/InitialEntities.java index 5c522d4f0..168532f38 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/InitialEntities.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/InitialEntities.java @@ -24,10 +24,18 @@ import org.springframework.data.neo4j.core.schema.Node; */ public class InitialEntities { + /** + * Parallel node type + */ + @Node + public interface SpecialKind { + + } + /** * Base */ - public static abstract class AbstractBase { + public abstract static class AbstractBase { @Id @GeneratedValue(generatorClass = SomeStringGenerator.class) @@ -39,15 +47,9 @@ public class InitialEntities { * This is where the repository accesses the domain. */ @Node - public static abstract class SomethingInBetween extends AbstractBase { - public String name; - } + public abstract static class SomethingInBetween extends AbstractBase { - /** - * Parallel node type - */ - @Node - public interface SpecialKind { + public String name; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomeStringGenerator.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomeStringGenerator.java index 1a792a637..2611b27bc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomeStringGenerator.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomeStringGenerator.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2530; -import org.springframework.data.neo4j.core.schema.IdGenerator; - import java.util.UUID; +import org.springframework.data.neo4j.core.schema.IdGenerator; + /** * Generator to mimic the reported behaviour. */ @@ -28,4 +28,5 @@ public class SomeStringGenerator implements IdGenerator { public String generateId(String primaryLabel, Object entity) { return primaryLabel + UUID.randomUUID(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomethingInBetweenRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomethingInBetweenRepository.java index 4162ea201..a0916c934 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomethingInBetweenRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2530/SomethingInBetweenRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface SomethingInBetweenRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/EntitiesAndProjections.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/EntitiesAndProjections.java index 8414aa3e0..3eb7010d1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/EntitiesAndProjections.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/EntitiesAndProjections.java @@ -15,6 +15,9 @@ */ package org.springframework.data.neo4j.integration.issues.gh2533; +import java.util.List; +import java.util.Map; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -23,19 +26,67 @@ import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; -import java.util.List; -import java.util.Map; - /** * Collection of entities for GH2533. */ public class EntitiesAndProjections { + /** + * Projection breaking the infinite loop + */ + public interface GH2533EntityWithoutRelationship { + + Long getId(); + + String getName(); + + } + + /** + * Projection with one level of relationship + */ + public interface GH2533EntityNodeWithOneLevelLinks { + + Long getId(); + + String getName(); + + Map> getRelationships(); + + } + + /** + * Projection of the relationship properties + */ + public interface GH2533RelationshipWithoutTargetRelationships { + + Long getId(); + + boolean isActive(); + + GH2533EntityWithoutRelationship getTarget(); + + } + + /** + * Projection to entity + */ + public interface GH2533EntityWithRelationshipToEntity { + + Long getId(); + + String getName(); + + Map> getRelationships(); + + } + /** * Entity */ @Node public static class GH2533Entity { + @Id @GeneratedValue public Long id; @@ -44,6 +95,7 @@ public class EntitiesAndProjections { @Relationship public Map> relationships; + } /** @@ -51,52 +103,13 @@ public class EntitiesAndProjections { */ @RelationshipProperties public static class GH2533Relationship { + @RelationshipId public Long id; @TargetNode public GH2533Entity target; + } - /** - * Projection breaking the infinite loop - */ - public interface GH2533EntityWithoutRelationship { - Long getId(); - - String getName(); - } - - /** - * Projection with one level of relationship - */ - public interface GH2533EntityNodeWithOneLevelLinks { - Long getId(); - - String getName(); - - Map> getRelationships(); - } - - /** - * Projection of the relationship properties - */ - public interface GH2533RelationshipWithoutTargetRelationships { - Long getId(); - - boolean isActive(); - - GH2533EntityWithoutRelationship getTarget(); - } - - /** - * Projection to entity - */ - public interface GH2533EntityWithRelationshipToEntity { - Long getId(); - - String getName(); - - Map> getRelationships(); - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/GH2533Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/GH2533Repository.java index 2f3064a86..99bf888fd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/GH2533Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/GH2533Repository.java @@ -28,4 +28,5 @@ public interface GH2533Repository extends Neo4jRepository(m) WHERE id(n)=$id RETURN n, collect(relationships(p)), collect(m);") Optional findByIdWithLevelOneLinks(@Param("id") Long id); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/ReactiveGH2533Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/ReactiveGH2533Repository.java index 4e65058ea..17070f750 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/ReactiveGH2533Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2533/ReactiveGH2533Repository.java @@ -28,4 +28,5 @@ public interface ReactiveGH2533Repository extends ReactiveNeo4jRepository(m) WHERE id(n)=$id RETURN n, collect(relationships(p)), collect(m);") Mono findByIdWithLevelOneLinks(@Param("id") Long id); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNode.java index 83c6d7641..2329baf5f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNode.java @@ -25,21 +25,22 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class TestNode { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - public TestNode(String name) { this.name = name; } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNodeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNodeRepository.java index 1ba53828b..fb1644a98 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNodeRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2542/TestNodeRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael J. Simons */ public interface TestNodeRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572BaseEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572BaseEntity.java index a2aacc1c2..4d2a88efc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572BaseEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572BaseEntity.java @@ -19,12 +19,13 @@ import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; /** + * @param the concrete type * @author Michael J. Simons - * @param The concrete type */ abstract class GH2572BaseEntity> { @Id - @GeneratedValue(value = MyStrategy.class) + @GeneratedValue(MyStrategy.class) protected String id; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Child.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Child.java index 71fdffda1..2125d02fe 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Child.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Child.java @@ -23,6 +23,7 @@ import org.springframework.data.neo4j.core.schema.Relationship; */ @Node public class GH2572Child extends GH2572BaseEntity { + private String name; @Relationship(value = "IS_PET", direction = Relationship.Direction.OUTGOING) @@ -40,15 +41,16 @@ public class GH2572Child extends GH2572BaseEntity { return this.name; } - public GH2572Parent getOwner() { - return this.owner; - } - public void setName(String name) { this.name = name; } + public GH2572Parent getOwner() { + return this.owner; + } + public void setOwner(GH2572Parent owner) { this.owner = owner; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Parent.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Parent.java index e7a7315ac..be9536c00 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Parent.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Parent.java @@ -39,15 +39,16 @@ public class GH2572Parent extends GH2572BaseEntity { return this.name; } - public int getAge() { - return this.age; - } - public void setName(String name) { this.name = name; } + public int getAge() { + return this.age; + } + public void setAge(int age) { this.age = age; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Repository.java index b3caa23c5..6165b5c40 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/GH2572Repository.java @@ -26,18 +26,16 @@ import org.springframework.data.neo4j.repository.query.Query; */ public interface GH2572Repository extends Neo4jRepository { - @Query("MATCH(person:GH2572Parent {id: $id}) " - + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + @Query("MATCH(person:GH2572Parent {id: $id}) " + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + "RETURN dog") List getDogsForPerson(String id); - @Query("MATCH(person:GH2572Parent {id: $id}) " - + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + @Query("MATCH(person:GH2572Parent {id: $id}) " + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + "RETURN dog ORDER BY dog.name ASC LIMIT 1") Optional findOneDogForPerson(String id); - @Query("MATCH(person:GH2572Parent {id: $id}) " - + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + @Query("MATCH(person:GH2572Parent {id: $id}) " + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + "RETURN dog ORDER BY dog.name ASC LIMIT 1") GH2572Child getOneDogForPerson(String id); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/MyStrategy.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/MyStrategy.java index 9eec08582..2ad4714e8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/MyStrategy.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/MyStrategy.java @@ -20,7 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.springframework.data.neo4j.core.schema.IdGenerator; /** - * Not actually needed during the test, but added to recreate the issue scenario as much as possible. + * Not actually needed during the test, but added to recreate the issue scenario as much + * as possible. * * @author Michael J. Simons */ @@ -30,6 +31,7 @@ public final class MyStrategy implements IdGenerator { @Override public String generateId(String primaryLabel, Object entity) { - return primaryLabel + sequence.incrementAndGet(); + return primaryLabel + this.sequence.incrementAndGet(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/ReactiveGH2572Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/ReactiveGH2572Repository.java index 0c2abdc2f..e843d1e29 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/ReactiveGH2572Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2572/ReactiveGH2572Repository.java @@ -26,13 +26,12 @@ import org.springframework.data.neo4j.repository.query.Query; */ public interface ReactiveGH2572Repository extends ReactiveNeo4jRepository { - @Query("MATCH(person:GH2572Parent {id: $id}) " - + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + @Query("MATCH(person:GH2572Parent {id: $id}) " + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + "RETURN dog") Flux getDogsForPerson(String id); - @Query("MATCH(person:GH2572Parent {id: $id}) " - + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + @Query("MATCH(person:GH2572Parent {id: $id}) " + "OPTIONAL MATCH (person)<-[:IS_PET]-(dog:GH2572Child) " + "RETURN dog ORDER BY dog.name ASC LIMIT 1") Mono findOneDogForPerson(String id); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/College.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/College.java index 20b843f7b..51f5e20b8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/College.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/College.java @@ -37,14 +37,15 @@ public class College { } public String getGuid() { - return guid; + return this.guid; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/CollegeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/CollegeRepository.java index 8932efbc3..2c7822efa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/CollegeRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/CollegeRepository.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import org.neo4j.driver.Value; + import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; @@ -31,15 +32,14 @@ public interface CollegeRepository extends Neo4jRepository { UNWIND $0 AS row MATCH (student:Student{guid:row.stuGuid}) MATCH (college:College{guid:row.collegeGuid}) - CREATE (student)<-[:STUDENT_OF]-(college) RETURN student.guid""" - ) + CREATE (student)<-[:STUDENT_OF]-(college) RETURN student.guid""") List addStudentToCollege(List> list); @Query(""" UNWIND $0 AS row MATCH (student:Student{guid:row.stuGuid}) MATCH (college:College{guid:row.collegeGuid}) - CREATE (student)<-[:STUDENT_OF]-(college) RETURN student.guid""" - ) + CREATE (student)<-[:STUDENT_OF]-(college) RETURN student.guid""") List addStudentToCollegeWorkaround(List list); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/Student.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/Student.java index f57079d0f..aa228f05f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/Student.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2576/Student.java @@ -41,11 +41,11 @@ public class Student { } public String getGuid() { - return guid; + return this.guid; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -53,10 +53,11 @@ public class Student { } public College getCollege() { - return college; + return this.college; } public void setCollege(College college) { this.college = college; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/ColumnNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/ColumnNode.java index 029809c37..c967f6d22 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/ColumnNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/ColumnNode.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2579; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -44,42 +46,47 @@ public class ColumnNode { return this.id; } - public String getSourceName() { - return this.sourceName; - } - - public String getSchemaName() { - return this.schemaName; - } - - public String getTableName() { - return this.tableName; - } - - public String getName() { - return this.name; - } - public void setId(Long id) { this.id = id; } + public String getSourceName() { + return this.sourceName; + } + public void setSourceName(String sourceName) { this.sourceName = sourceName; } + public String getSchemaName() { + return this.schemaName; + } + public void setSchemaName(String schemaName) { this.schemaName = schemaName; } + public String getTableName() { + return this.tableName; + } + public void setTableName(String tableName) { this.tableName = tableName; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + protected boolean canEqual(final Object other) { + return other instanceof ColumnNode; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -93,53 +100,50 @@ public class ColumnNode { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$sourceName = this.getSourceName(); final Object other$sourceName = other.getSourceName(); - if (this$sourceName == null ? other$sourceName != null : !this$sourceName.equals(other$sourceName)) { + if (!Objects.equals(this$sourceName, other$sourceName)) { return false; } final Object this$schemaName = this.getSchemaName(); final Object other$schemaName = other.getSchemaName(); - if (this$schemaName == null ? other$schemaName != null : !this$schemaName.equals(other$schemaName)) { + if (!Objects.equals(this$schemaName, other$schemaName)) { return false; } final Object this$tableName = this.getTableName(); final Object other$tableName = other.getTableName(); - if (this$tableName == null ? other$tableName != null : !this$tableName.equals(other$tableName)) { + if (!Objects.equals(this$tableName, other$tableName)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof ColumnNode; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $sourceName = this.getSourceName(); - result = result * PRIME + ($sourceName == null ? 43 : $sourceName.hashCode()); + result = result * PRIME + (($sourceName != null) ? $sourceName.hashCode() : 43); final Object $schemaName = this.getSchemaName(); - result = result * PRIME + ($schemaName == null ? 43 : $schemaName.hashCode()); + result = result * PRIME + (($schemaName != null) ? $schemaName.hashCode() : 43); final Object $tableName = this.getTableName(); - result = result * PRIME + ($tableName == null ? 43 : $tableName.hashCode()); + result = result * PRIME + (($tableName != null) ? $tableName.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); return result; } + @Override public String toString() { - return "ColumnNode(id=" + this.getId() + ", sourceName=" + this.getSourceName() + ", schemaName=" + this.getSchemaName() + ", tableName=" + this.getTableName() + ", name=" + this.getName() + ")"; + return "ColumnNode(id=" + this.getId() + ", sourceName=" + this.getSourceName() + ", schemaName=" + + this.getSchemaName() + ", tableName=" + this.getTableName() + ", name=" + this.getName() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableAndColumnRelation.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableAndColumnRelation.java index 54fac4e75..3fbedc9be 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableAndColumnRelation.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableAndColumnRelation.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2579; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; @@ -38,18 +40,23 @@ public class TableAndColumnRelation { return this.id; } - public ColumnNode getColumnNode() { - return this.columnNode; - } - public void setId(Long id) { this.id = id; } + public ColumnNode getColumnNode() { + return this.columnNode; + } + public void setColumnNode(ColumnNode columnNode) { this.columnNode = columnNode; } + protected boolean canEqual(final Object other) { + return other instanceof TableAndColumnRelation; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -63,32 +70,28 @@ public class TableAndColumnRelation { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$columnNode = this.getColumnNode(); final Object other$columnNode = other.getColumnNode(); - if (this$columnNode == null ? other$columnNode != null : !this$columnNode.equals(other$columnNode)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof TableAndColumnRelation; + return Objects.equals(this$columnNode, other$columnNode); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $columnNode = this.getColumnNode(); - result = result * PRIME + ($columnNode == null ? 43 : $columnNode.hashCode()); + result = (result * PRIME) + (($columnNode != null) ? $columnNode.hashCode() : 43); return result; } + @Override public String toString() { return "TableAndColumnRelation(id=" + this.getId() + ", columnNode=" + this.getColumnNode() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableNode.java index c20d56b91..264e24b8e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableNode.java @@ -15,13 +15,14 @@ */ package org.springframework.data.neo4j.integration.issues.gh2579; +import java.util.List; +import java.util.Objects; + 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.Relationship; -import java.util.List; - /** * @author Michael J. Simons */ @@ -50,50 +51,55 @@ public class TableNode { return this.id; } - public String getSourceName() { - return this.sourceName; - } - - public String getSchemaName() { - return this.schemaName; - } - - public String getName() { - return this.name; - } - - public String getTableComment() { - return this.tableComment; - } - - public List getTableAndColumnRelation() { - return this.tableAndColumnRelation; - } - public void setId(Long id) { this.id = id; } + public String getSourceName() { + return this.sourceName; + } + public void setSourceName(String sourceName) { this.sourceName = sourceName; } + public String getSchemaName() { + return this.schemaName; + } + public void setSchemaName(String schemaName) { this.schemaName = schemaName; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public String getTableComment() { + return this.tableComment; + } + public void setTableComment(String tableComment) { this.tableComment = tableComment; } + public List getTableAndColumnRelation() { + return this.tableAndColumnRelation; + } + public void setTableAndColumnRelation(List tableAndColumnRelation) { this.tableAndColumnRelation = tableAndColumnRelation; } + protected boolean canEqual(final Object other) { + return other instanceof TableNode; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -107,60 +113,58 @@ public class TableNode { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$sourceName = this.getSourceName(); final Object other$sourceName = other.getSourceName(); - if (this$sourceName == null ? other$sourceName != null : !this$sourceName.equals(other$sourceName)) { + if (!Objects.equals(this$sourceName, other$sourceName)) { return false; } final Object this$schemaName = this.getSchemaName(); final Object other$schemaName = other.getSchemaName(); - if (this$schemaName == null ? other$schemaName != null : !this$schemaName.equals(other$schemaName)) { + if (!Objects.equals(this$schemaName, other$schemaName)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$tableComment = this.getTableComment(); final Object other$tableComment = other.getTableComment(); - if (this$tableComment == null ? other$tableComment != null : !this$tableComment.equals(other$tableComment)) { + if (!Objects.equals(this$tableComment, other$tableComment)) { return false; } final Object this$tableAndColumnRelation = this.getTableAndColumnRelation(); final Object other$tableAndColumnRelation = other.getTableAndColumnRelation(); - if (this$tableAndColumnRelation == null ? other$tableAndColumnRelation != null : !this$tableAndColumnRelation.equals(other$tableAndColumnRelation)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof TableNode; + return Objects.equals(this$tableAndColumnRelation, other$tableAndColumnRelation); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $sourceName = this.getSourceName(); - result = result * PRIME + ($sourceName == null ? 43 : $sourceName.hashCode()); + result = (result * PRIME) + (($sourceName != null) ? $sourceName.hashCode() : 43); final Object $schemaName = this.getSchemaName(); - result = result * PRIME + ($schemaName == null ? 43 : $schemaName.hashCode()); + result = (result * PRIME) + (($schemaName != null) ? $schemaName.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); final Object $tableComment = this.getTableComment(); - result = result * PRIME + ($tableComment == null ? 43 : $tableComment.hashCode()); + result = (result * PRIME) + (($tableComment != null) ? $tableComment.hashCode() : 43); final Object $tableAndColumnRelation = this.getTableAndColumnRelation(); - result = result * PRIME + ($tableAndColumnRelation == null ? 43 : $tableAndColumnRelation.hashCode()); + result = (result * PRIME) + (($tableAndColumnRelation != null) ? $tableAndColumnRelation.hashCode() : 43); return result; } + @Override public String toString() { - return "TableNode(id=" + this.getId() + ", sourceName=" + this.getSourceName() + ", schemaName=" + this.getSchemaName() + ", name=" + this.getName() + ", tableComment=" + this.getTableComment() + ", tableAndColumnRelation=" + this.getTableAndColumnRelation() + ")"; + return "TableNode(id=" + this.getId() + ", sourceName=" + this.getSourceName() + ", schemaName=" + + this.getSchemaName() + ", name=" + this.getName() + ", tableComment=" + this.getTableComment() + + ", tableAndColumnRelation=" + this.getTableAndColumnRelation() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableRepository.java index db312978d..e4029bee7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2579/TableRepository.java @@ -26,7 +26,7 @@ import org.springframework.data.repository.query.Param; */ public interface TableRepository extends Neo4jRepository { - @Query(value = """ + @Query(""" UNWIND :#{#froms} AS col WITH col.__properties__ AS col, :#{#to}.__properties__ AS to MERGE (c:Column { @@ -40,7 +40,7 @@ public interface TableRepository extends Neo4jRepository { schemaName: to.schemaName, name: to.name }) - MERGE (c) -[r:BELONG]-> (t)""" - ) + MERGE (c) -[r:BELONG]-> (t)""") void mergeTableAndColumnRelations(@Param("froms") List froms, @Param("to") TableNode to); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Node.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Node.java index a94f16433..154e8d155 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Node.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Node.java @@ -15,25 +15,27 @@ */ package org.springframework.data.neo4j.integration.issues.gh2583; +import java.util.List; + 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.Relationship; -import java.util.List; - /** * A simple node with bidirectional relationship mapping to the very same type. */ @Node public class GH2583Node { - @Id - @GeneratedValue - Long id; @Relationship(type = "LINKED", direction = Relationship.Direction.OUTGOING) public List outgoingNodes; @Relationship(type = "LINKED", direction = Relationship.Direction.INCOMING) public List incomingNodes; + + @Id + @GeneratedValue + Long id; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Repository.java index 9087f2d01..27640c18a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2583/GH2583Repository.java @@ -25,10 +25,9 @@ import org.springframework.data.neo4j.repository.query.Query; */ public interface GH2583Repository extends Neo4jRepository { - @Query(value = "MATCH (s:GH2583Node) " + - "WITH s OPTIONAL MATCH (s)-[r:LINKED]->(t:GH2583Node) " + - "RETURN s, collect(r), collect(t) " + - ":#{orderBy(#pageable)} SKIP $skip LIMIT $limit", + @Query(value = "MATCH (s:GH2583Node) " + "WITH s OPTIONAL MATCH (s)-[r:LINKED]->(t:GH2583Node) " + + "RETURN s, collect(r), collect(t) " + ":#{orderBy(#pageable)} SKIP $skip LIMIT $limit", countQuery = "MATCH (s:hktxjm) RETURN count(s)") Page getNodesByCustomQuery(Pageable pageable); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/GH2622Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/GH2622Repository.java index 3e6efea10..75337b2d4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/GH2622Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/GH2622Repository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface GH2622Repository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/MePointingTowardsMe.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/MePointingTowardsMe.java index fa67879f6..55ef66a6e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/MePointingTowardsMe.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2622/MePointingTowardsMe.java @@ -15,28 +15,28 @@ */ package org.springframework.data.neo4j.integration.issues.gh2622; +import java.util.List; +import java.util.Objects; + 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.Relationship; -import java.util.List; -import java.util.Objects; - /** * @author Gerrit Meier */ @Node public class MePointingTowardsMe { - @Id - @GeneratedValue - Long id; + @Relationship + public final List others; final String name; - @Relationship - public final List others; + @Id + @GeneratedValue + Long id; public MePointingTowardsMe(String name, List others) { this.name = name; @@ -52,11 +52,12 @@ public class MePointingTowardsMe { return false; } MePointingTowardsMe that = (MePointingTowardsMe) o; - return name.equals(that.name); + return this.name.equals(that.name); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(this.name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/Movie.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/Movie.java index 60093710e..408472e91 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/Movie.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/Movie.java @@ -34,14 +34,15 @@ public class Movie { private String title; public UUID getId() { - return id; + return this.id; } public String getTitle() { - return title; + return this.title; } public void setTitle(String title) { this.title = title; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/MovieRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/MovieRepository.java index 32d4e7440..5404f7d03 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/MovieRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/MovieRepository.java @@ -23,4 +23,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Michael J. Simons */ public interface MovieRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/ReactiveConnectionAcquisitionIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/ReactiveConnectionAcquisitionIT.java index e5d8168a5..f7814683e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/ReactiveConnectionAcquisitionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2632/ReactiveConnectionAcquisitionIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues.gh2632; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -31,6 +29,10 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.reactivestreams.ReactiveResult; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,9 +42,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.annotation.EnableTransactionManagement; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -59,23 +59,22 @@ class ReactiveConnectionAcquisitionIT { transaction.run("MATCH (n) detach delete n"); transaction.run(""" CREATE (m:Movie {title: "I don't want warnings", id: randomUUID()}) - """ - ).consume(); + """).consume(); transaction.commit(); } } } @Test - // GH-2632 - void connectionAcquisitionAfterErrorViaSDNTxManagerShouldWork(@Autowired MovieRepository movieRepository, @Autowired Driver driver) { + // GH-2632 + void connectionAcquisitionAfterErrorViaSDNTxManagerShouldWork(@Autowired MovieRepository movieRepository, + @Autowired Driver driver) { UUID id = UUID.randomUUID(); - Flux - .range(1, 5) - .flatMap(i -> movieRepository.findById(id).switchIfEmpty(Mono.error(new RuntimeException()))) - .then() - .as(StepVerifier::create) - .verifyError(); + Flux.range(1, 5) + .flatMap(i -> movieRepository.findById(id).switchIfEmpty(Mono.error(new RuntimeException()))) + .then() + .as(StepVerifier::create) + .verifyError(); try (Session session = driver.session()) { long aNumber = session.run("RETURN 1").single().get(0).asLong(); @@ -84,25 +83,41 @@ class ReactiveConnectionAcquisitionIT { } @Test - // GH-2632 + // GH-2632 void connectionAcquisitionAfterErrorViaImplicitTXShouldWork(@Autowired Driver driver) { - Flux - .range(1, 5) - .flatMap( - i -> { - Query query = new Query("MATCH (p:Product) WHERE p.id = $id RETURN p.title", Collections.singletonMap("id", 0)); - return Flux.usingWhen( - Mono.fromSupplier(() -> driver.session(ReactiveSession.class)), - session -> Flux.from(session.run(query)) - .flatMap(result -> Flux.from(result.records())) - .map(record -> record.get(0).asString()), - session -> Mono.fromDirect(session.close()) - ).switchIfEmpty(Mono.error(new RuntimeException())); - } - ) - .then() - .as(StepVerifier::create) - .verifyError(); + Flux.range(1, 5).flatMap(i -> { + Query query = new Query("MATCH (p:Product) WHERE p.id = $id RETURN p.title", + Collections.singletonMap("id", 0)); + return Flux + .usingWhen(Mono.fromSupplier(() -> driver.session(ReactiveSession.class)), + session -> Flux.from(session.run(query)) + .flatMap(result -> Flux.from(result.records())) + .map(record -> record.get(0).asString()), + session -> Mono.fromDirect(session.close())) + .switchIfEmpty(Mono.error(new RuntimeException())); + }).then().as(StepVerifier::create).verifyError(); + + try (Session session = driver.session()) { + long aNumber = session.run("RETURN 1").single().get(0).asLong(); + assertThat(aNumber).isOne(); + } + } + + @Test + // GH-2632 + void connectionAcquisitionAfterErrorViaExplicitTXShouldWork(@Autowired Driver driver) { + Flux.range(1, 5).flatMap(i -> { + Mono f = Mono.just(driver.session(ReactiveSession.class)) + .flatMap(s -> Mono.fromDirect(s.beginTransaction()).map(tx -> new SessionAndTx(s, tx))); + return Flux + .usingWhen(f, + h -> Flux.from(h.tx.run("MATCH (n) WHERE false = true RETURN n")) + .flatMap(ReactiveResult::records), + h -> Mono.from(h.tx.commit()).then(Mono.from(h.session.close())), + (h, e) -> Mono.from(h.tx.rollback()).then(Mono.from(h.session.close())), + h -> Mono.from(h.tx.rollback()).then(Mono.from(h.session.close()))) + .switchIfEmpty(Mono.error(new RuntimeException())); + }).then().as(StepVerifier::create).verifyError(); try (Session session = driver.session()) { long aNumber = session.run("RETURN 1").single().get(0).asLong(); @@ -113,47 +128,22 @@ class ReactiveConnectionAcquisitionIT { record SessionAndTx(ReactiveSession session, ReactiveTransaction tx) { } - @Test - // GH-2632 - void connectionAcquisitionAfterErrorViaExplicitTXShouldWork(@Autowired Driver driver) { - Flux - .range(1, 5) - .flatMap( - i -> { - Mono f = Mono - .just(driver.session(ReactiveSession.class)) - .flatMap(s -> Mono.fromDirect(s.beginTransaction()).map(tx -> new SessionAndTx(s, tx))); - return Flux.usingWhen(f, - h -> Flux.from(h.tx.run("MATCH (n) WHERE false = true RETURN n")).flatMap(ReactiveResult::records), - h -> Mono.from(h.tx.commit()).then(Mono.from(h.session.close())), - (h, e) -> Mono.from(h.tx.rollback()).then(Mono.from(h.session.close())), - h -> Mono.from(h.tx.rollback()).then(Mono.from(h.session.close())) - ).switchIfEmpty(Mono.error(new RuntimeException())); - } - ) - .then() - .as(StepVerifier::create) - .verifyError(); - - try (Session session = driver.session()) { - long aNumber = session.run("RETURN 1").single().get(0).asLong(); - assertThat(aNumber).isOne(); - } - } - @Configuration @EnableTransactionManagement @EnableReactiveNeo4jRepositories static class Config extends AbstractReactiveNeo4jConfig { @Bean + @Override public Driver driver() { var config = org.neo4j.driver.Config.builder() - .withMaxConnectionPoolSize(2) - .withConnectionAcquisitionTimeout(2, TimeUnit.SECONDS) - .withLeakedSessionsLogging() - .build(); + .withMaxConnectionPoolSize(2) + .withConnectionAcquisitionTimeout(2, TimeUnit.SECONDS) + .withLeakedSessionsLogging() + .build(); return GraphDatabase.driver(neo4jConnectionSupport.uri, neo4jConnectionSupport.authToken, config); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Company.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Company.java index f883cd4c1..18e30ee9c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Company.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Company.java @@ -15,28 +15,28 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; +import java.util.List; +import java.util.StringJoiner; + 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.Relationship; -import java.util.List; -import java.util.StringJoiner; - /** * Root node */ @Node public class Company { - @Id - @GeneratedValue - private Long id; - private final String name; + @Relationship(type = "EMPLOYEE") private final List employees; + @Id + @GeneratedValue + private Long id; public Company(String name, List employees) { this.name = name; @@ -44,19 +44,19 @@ public class Company { } public void addEmployee(CompanyPerson person) { - employees.add(person); + this.employees.add(person); } public List getEmployees() { - return employees; + return this.employees; } @Override public String toString() { - return new StringJoiner(", ", Company.class.getSimpleName() + "[", "]") - .add("id=" + id) - .add("name='" + name + "'") - .add("employees=" + employees) - .toString(); + return new StringJoiner(", ", Company.class.getSimpleName() + "[", "]").add("id=" + this.id) + .add("name='" + this.name + "'") + .add("employees=" + this.employees) + .toString(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/CompanyRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/CompanyRepository.java index 141efdcfe..5f23952eb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/CompanyRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/CompanyRepository.java @@ -21,5 +21,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface CompanyRepository extends Neo4jRepository { + Company findByName(String name); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Developer.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Developer.java index a6fca775e..e74b27205 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Developer.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Developer.java @@ -15,19 +15,19 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; -import org.springframework.data.neo4j.core.schema.Node; - import java.util.List; import java.util.StringJoiner; +import org.springframework.data.neo4j.core.schema.Node; + /** - * Developer holds the specific relationship we are trying to map - * in this test case. + * Developer holds the specific relationship we are trying to map in this test case. */ @Node public class Developer extends CompanyPerson { private final List programmingLanguages; + private final String name; public Developer(String name, List programmingLanguages) { @@ -36,18 +36,18 @@ public class Developer extends CompanyPerson { } public List getProgrammingLanguages() { - return programmingLanguages; + return this.programmingLanguages; } public String getName() { - return name; + return this.name; } @Override public String toString() { - return new StringJoiner(", ", Developer.class.getSimpleName() + "[", "]") - .add("name='" + name + "'") - .add("programmingLanguages=" + programmingLanguages) - .toString(); + return new StringJoiner(", ", Developer.class.getSimpleName() + "[", "]").add("name='" + this.name + "'") + .add("programmingLanguages=" + this.programmingLanguages) + .toString(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Enterprise.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Enterprise.java index 6d517390e..0242d4d64 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Enterprise.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Enterprise.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; -import org.springframework.data.neo4j.core.schema.Node; - import java.util.Objects; +import org.springframework.data.neo4j.core.schema.Node; + /** * @author Gerrit Meier */ @@ -41,11 +41,12 @@ public class Enterprise extends Inventor { return false; } Enterprise that = (Enterprise) o; - return someEnterpriseProperty.equals(that.someEnterpriseProperty); + return this.someEnterpriseProperty.equals(that.someEnterpriseProperty); } @Override public int hashCode() { - return Objects.hash(someEnterpriseProperty); + return Objects.hash(this.someEnterpriseProperty); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Individual.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Individual.java index 575cee967..50a1911bc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Individual.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Individual.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; -import org.springframework.data.neo4j.core.schema.Node; - import java.util.Objects; +import org.springframework.data.neo4j.core.schema.Node; + /** * @author Gerrit Meier */ @@ -41,11 +41,12 @@ public class Individual extends Inventor { return false; } Individual that = (Individual) o; - return username.equals(that.username); + return this.username.equals(that.username); } @Override public int hashCode() { - return Objects.hash(username); + return Objects.hash(this.username); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Inventor.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Inventor.java index 7e2021086..3b41bff62 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Inventor.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Inventor.java @@ -30,4 +30,5 @@ public abstract class Inventor { public Inventor(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/LanguageRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/LanguageRelationship.java index efd9f881b..77077ac78 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/LanguageRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/LanguageRelationship.java @@ -25,16 +25,17 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class LanguageRelationship { - @RelationshipId - private Long id; - private final int score; @TargetNode private final ProgrammingLanguage language; + @RelationshipId + private Long id; + public LanguageRelationship(int score, ProgrammingLanguage language) { this.score = score; this.language = language; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/ProgrammingLanguage.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/ProgrammingLanguage.java index 148624a04..194a00769 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/ProgrammingLanguage.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/ProgrammingLanguage.java @@ -15,48 +15,49 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; +import java.util.StringJoiner; + 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.Relationship; -import java.util.StringJoiner; - /** - * Programming language to represent. - * Only available at the Developer entity. + * Programming language to represent. Only available at the Developer entity. */ @Node public class ProgrammingLanguage { - @Id - @GeneratedValue - private Long id; private final String name; + private final String version; @Relationship("INVENTED_BY") public Inventor inventor; + @Id + @GeneratedValue + private Long id; + public ProgrammingLanguage(String name, String version) { this.name = name; this.version = version; } public String getName() { - return name; + return this.name; } public String getVersion() { - return version; + return this.version; } @Override public String toString() { - return new StringJoiner(", ", ProgrammingLanguage.class.getSimpleName() + "[", "]") - .add("id=" + id) - .add("name='" + name + "'") - .add("version='" + version + "'") - .toString(); + return new StringJoiner(", ", ProgrammingLanguage.class.getSimpleName() + "[", "]").add("id=" + this.id) + .add("name='" + this.name + "'") + .add("version='" + this.version + "'") + .toString(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Sales.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Sales.java index aa7fb0590..2e30714ec 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Sales.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2639/Sales.java @@ -15,10 +15,10 @@ */ package org.springframework.data.neo4j.integration.issues.gh2639; -import org.springframework.data.neo4j.core.schema.Node; - import java.util.StringJoiner; +import org.springframework.data.neo4j.core.schema.Node; + /** * Sales person, some noise for the developer and company's generic person relationship. */ @@ -32,13 +32,13 @@ public class Sales extends CompanyPerson { } public String getName() { - return name; + return this.name; } @Override public String toString() { - return new StringJoiner(", ", Sales.class.getSimpleName() + "[", "]") - .add("name='" + name + "'") - .toString(); + return new StringJoiner(", ", Sales.class.getSimpleName() + "[", "]").add("name='" + this.name + "'") + .toString(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2640/PersistentPropertyCharacteristicsIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2640/PersistentPropertyCharacteristicsIT.java index f60c0c617..3447cc664 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2640/PersistentPropertyCharacteristicsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2640/PersistentPropertyCharacteristicsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues.gh2640; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.List; import java.util.Map; @@ -28,6 +26,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,6 +47,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -66,20 +67,23 @@ class PersistentPropertyCharacteristicsIT { } @Test - // GH-2640 - void implicitTransientPropertiesShouldNotBeWritten(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture, @Autowired Neo4jTemplate template) { + // GH-2640 + void implicitTransientPropertiesShouldNotBeWritten(@Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Neo4jTemplate template) { - SomeWithImplicitTransientProperties1 node1 = template.save(new SomeWithImplicitTransientProperties1("the name", 1.23)); + SomeWithImplicitTransientProperties1 node1 = template + .save(new SomeWithImplicitTransientProperties1("the name", 1.23)); SomeWithImplicitTransientProperties2 node2 = template.save(new SomeWithImplicitTransientProperties2(47.11)); try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List records = session.run("MATCH (n) WHERE n.id IN $ids RETURN n", Values.parameters("ids", Arrays.asList(node1.id.toString(), node2.id.toString()))).list(); - assertThat(records) - .hasSize(2) - .noneMatch(r -> { - Map properties = r.get("n").asNode().asMap(); - return properties.containsKey("foobar") || properties.containsKey("bazbar"); - }); + List records = session + .run("MATCH (n) WHERE n.id IN $ids RETURN n", + Values.parameters("ids", Arrays.asList(node1.id.toString(), node2.id.toString()))) + .list(); + assertThat(records).hasSize(2).noneMatch(r -> { + Map properties = r.get("n").asNode().asMap(); + return properties.containsKey("foobar") || properties.containsKey("bazbar"); + }); } } @@ -98,6 +102,7 @@ class PersistentPropertyCharacteristicsIT { this.name = name; this.foobar = foobar; } + } @Node @@ -112,6 +117,7 @@ class PersistentPropertyCharacteristicsIT { SomeWithImplicitTransientProperties2(Double bazbar) { this.bazbar = bazbar; } + } @Configuration @@ -120,13 +126,13 @@ class PersistentPropertyCharacteristicsIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager( - Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -134,7 +140,7 @@ class PersistentPropertyCharacteristicsIT { } @Bean - public PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider() { + PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider() { return (property, owner) -> { if (property.getType().equals(Double.class)) { @@ -146,6 +152,7 @@ class PersistentPropertyCharacteristicsIT { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -155,5 +162,7 @@ class PersistentPropertyCharacteristicsIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntity.java index eed944933..254489df9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntity.java @@ -15,19 +15,21 @@ */ package org.springframework.data.neo4j.integration.issues.gh2727; +import java.util.List; +import java.util.Objects; + 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.Relationship; -import java.util.List; - /** * @author Gerrit Meier */ @SuppressWarnings("HiddenField") @Node("FirstLevel") public class FirstLevelEntity { + @Id @GeneratedValue private Long id; @@ -54,26 +56,32 @@ public class FirstLevelEntity { return this.id; } - public String getName() { - return this.name; - } - - public List getSecondLevelEntityRelationshipProperties() { - return this.secondLevelEntityRelationshipProperties; - } - public void setId(Long id) { this.id = id; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } - public void setSecondLevelEntityRelationshipProperties(List secondLevelEntityRelationshipProperties) { + public List getSecondLevelEntityRelationshipProperties() { + return this.secondLevelEntityRelationshipProperties; + } + + public void setSecondLevelEntityRelationshipProperties( + List secondLevelEntityRelationshipProperties) { this.secondLevelEntityRelationshipProperties = secondLevelEntityRelationshipProperties; } + protected boolean canEqual(final Object other) { + return other instanceof FirstLevelEntity; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -87,32 +95,30 @@ public class FirstLevelEntity { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof FirstLevelEntity; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); return result; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class FirstLevelEntityBuilder> { + public abstract static class FirstLevelEntityBuilder> { + private Long id; + private String name; + private List secondLevelEntityRelationshipProperties; public B id(Long id) { @@ -125,7 +131,8 @@ public class FirstLevelEntity { return self(); } - public B secondLevelEntityRelationshipProperties(List secondLevelEntityRelationshipProperties) { + public B secondLevelEntityRelationshipProperties( + List secondLevelEntityRelationshipProperties) { this.secondLevelEntityRelationshipProperties = secondLevelEntityRelationshipProperties; return self(); } @@ -134,21 +141,30 @@ public class FirstLevelEntity { public abstract C build(); + @Override public String toString() { - return "FirstLevelEntity.FirstLevelEntityBuilder(id=" + this.id + ", name=" + this.name + ", secondLevelEntityRelationshipProperties=" + this.secondLevelEntityRelationshipProperties + ")"; + return "FirstLevelEntity.FirstLevelEntityBuilder(id=" + this.id + ", name=" + this.name + + ", secondLevelEntityRelationshipProperties=" + this.secondLevelEntityRelationshipProperties + ")"; } + } - private static final class FirstLevelEntityBuilderImpl extends FirstLevelEntityBuilder { + private static final class FirstLevelEntityBuilderImpl + extends FirstLevelEntityBuilder { + private FirstLevelEntityBuilderImpl() { } + @Override protected FirstLevelEntityBuilderImpl self() { return this; } + @Override public FirstLevelEntity build() { return new FirstLevelEntity(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntityRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntityRepository.java index 45addbcc7..444b5aed6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntityRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelEntityRepository.java @@ -23,4 +23,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; public interface FirstLevelEntityRepository extends Neo4jRepository { FirstLevelProjection findOneById(Long id); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelProjection.java index 89da099c3..5de5db0b0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/FirstLevelProjection.java @@ -21,6 +21,7 @@ import java.util.Set; * @author Gerrit Meier */ public interface FirstLevelProjection { + Long getId(); Set getSecondLevelEntityRelationshipProperties(); @@ -29,10 +30,13 @@ public interface FirstLevelProjection { * */ interface SecondLevelRelationshipProjection { + Long getId(); SecondLevelProjection getTarget(); Integer getOrder(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/OrderedRelation.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/OrderedRelation.java index 29d523a37..399911b93 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/OrderedRelation.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/OrderedRelation.java @@ -22,16 +22,19 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * @author Gerrit Meier * @param relationship properties type + * @author Gerrit Meier */ @RelationshipProperties public class OrderedRelation implements Comparable> { + @RelationshipId @GeneratedValue private Long id; + @TargetNode private T target; + @Property private Integer order; @@ -46,30 +49,31 @@ public class OrderedRelation implements Comparable> { @Override public int compareTo(final OrderedRelation o) { - return order - o.order; + return this.order - o.order; } public Long getId() { return this.id; } - public T getTarget() { - return this.target; - } - - public Integer getOrder() { - return this.order; - } - public void setId(Long id) { this.id = id; } + public T getTarget() { + return this.target; + } + public void setTarget(T target) { this.target = target; } + public Integer getOrder() { + return this.order; + } + public void setOrder(Integer order) { this.order = order; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntity.java index 108a1fb3c..5a88478b6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntity.java @@ -15,19 +15,21 @@ */ package org.springframework.data.neo4j.integration.issues.gh2727; +import java.util.List; +import java.util.Objects; + 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.Relationship; -import java.util.List; - /** * @author Gerrit Meier */ @SuppressWarnings("HiddenField") @Node("SecondLevel") public class SecondLevelEntity { + @Id @GeneratedValue private Long id; @@ -54,26 +56,32 @@ public class SecondLevelEntity { return this.id; } - public String getSomeValue() { - return this.someValue; - } - - public List getThirdLevelEntityRelationshipProperties() { - return this.thirdLevelEntityRelationshipProperties; - } - public void setId(Long id) { this.id = id; } + public String getSomeValue() { + return this.someValue; + } + public void setSomeValue(String someValue) { this.someValue = someValue; } - public void setThirdLevelEntityRelationshipProperties(List thirdLevelEntityRelationshipProperties) { + public List getThirdLevelEntityRelationshipProperties() { + return this.thirdLevelEntityRelationshipProperties; + } + + public void setThirdLevelEntityRelationshipProperties( + List thirdLevelEntityRelationshipProperties) { this.thirdLevelEntityRelationshipProperties = thirdLevelEntityRelationshipProperties; } + protected boolean canEqual(final Object other) { + return other instanceof SecondLevelEntity; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -87,32 +95,30 @@ public class SecondLevelEntity { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof SecondLevelEntity; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); return result; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class SecondLevelEntityBuilder> { + public abstract static class SecondLevelEntityBuilder> { + private Long id; + private String someValue; + private List thirdLevelEntityRelationshipProperties; public B id(Long id) { @@ -125,7 +131,8 @@ public class SecondLevelEntity { return self(); } - public B thirdLevelEntityRelationshipProperties(List thirdLevelEntityRelationshipProperties) { + public B thirdLevelEntityRelationshipProperties( + List thirdLevelEntityRelationshipProperties) { this.thirdLevelEntityRelationshipProperties = thirdLevelEntityRelationshipProperties; return self(); } @@ -134,21 +141,30 @@ public class SecondLevelEntity { public abstract C build(); + @Override public String toString() { - return "SecondLevelEntity.SecondLevelEntityBuilder(id=" + this.id + ", someValue=" + this.someValue + ", thirdLevelEntityRelationshipProperties=" + this.thirdLevelEntityRelationshipProperties + ")"; + return "SecondLevelEntity.SecondLevelEntityBuilder(id=" + this.id + ", someValue=" + this.someValue + + ", thirdLevelEntityRelationshipProperties=" + this.thirdLevelEntityRelationshipProperties + ")"; } + } - private static final class SecondLevelEntityBuilderImpl extends SecondLevelEntityBuilder { + private static final class SecondLevelEntityBuilderImpl + extends SecondLevelEntityBuilder { + private SecondLevelEntityBuilderImpl() { } + @Override protected SecondLevelEntityBuilderImpl self() { return this; } + @Override public SecondLevelEntity build() { return new SecondLevelEntity(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntityRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntityRelationship.java index cf5a01530..c3613ec16 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntityRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelEntityRelationship.java @@ -19,4 +19,5 @@ package org.springframework.data.neo4j.integration.issues.gh2727; * @author Gerrit Meier */ public class SecondLevelEntityRelationship extends OrderedRelation { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelProjection.java index 174f0705b..57f2e0dbd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/SecondLevelProjection.java @@ -21,6 +21,7 @@ import java.util.Set; * @author Gerrit Meier */ public interface SecondLevelProjection { + Long getId(); String getSomeValue(); @@ -31,10 +32,13 @@ public interface SecondLevelProjection { * */ interface ThirdLevelRelationshipProjection { + Long getId(); ThirdLevelProjection getTarget(); Integer getOrder(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntity.java index da41ee55b..192b68552 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntity.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.issues.gh2727; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -25,6 +27,7 @@ import org.springframework.data.neo4j.core.schema.Node; @SuppressWarnings("HiddenField") @Node("ThirdLevel") public class ThirdLevelEntity { + @Id @GeneratedValue private Long id; @@ -47,18 +50,23 @@ public class ThirdLevelEntity { return this.id; } - public String getSomeValue() { - return this.someValue; - } - public void setId(Long id) { this.id = id; } + public String getSomeValue() { + return this.someValue; + } + public void setSomeValue(String someValue) { this.someValue = someValue; } + protected boolean canEqual(final Object other) { + return other instanceof ThirdLevelEntity; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -72,31 +80,28 @@ public class ThirdLevelEntity { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof ThirdLevelEntity; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); return result; } /** * the builder + * * @param needed c type * @param needed b type */ - public static abstract class ThirdLevelEntityBuilder> { + public abstract static class ThirdLevelEntityBuilder> { + private Long id; + private String someValue; public B id(Long id) { @@ -113,21 +118,29 @@ public class ThirdLevelEntity { public abstract C build(); + @Override public String toString() { return "ThirdLevelEntity.ThirdLevelEntityBuilder(id=" + this.id + ", someValue=" + this.someValue + ")"; } + } - private static final class ThirdLevelEntityBuilderImpl extends ThirdLevelEntityBuilder { + private static final class ThirdLevelEntityBuilderImpl + extends ThirdLevelEntityBuilder { + private ThirdLevelEntityBuilderImpl() { } + @Override protected ThirdLevelEntityBuilderImpl self() { return this; } + @Override public ThirdLevelEntity build() { return new ThirdLevelEntity(this); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntityRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntityRelationship.java index ff6959653..a62176658 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntityRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelEntityRelationship.java @@ -19,4 +19,5 @@ package org.springframework.data.neo4j.integration.issues.gh2727; * @author Gerrit Meier */ public class ThirdLevelEntityRelationship extends OrderedRelation { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelProjection.java index 3c5e9a4f3..3b8d93d52 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2727/ThirdLevelProjection.java @@ -19,7 +19,9 @@ package org.springframework.data.neo4j.integration.issues.gh2727; * @author Gerrit Meier */ public interface ThirdLevelProjection { + Long getId(); String getSomeValue(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractReactiveTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractReactiveTestBase.java index 065876edf..54718f6aa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractReactiveTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractReactiveTestBase.java @@ -15,9 +15,9 @@ */ package org.springframework.data.neo4j.integration.issues.gh2728; -import org.junit.jupiter.api.Assertions; 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.data.neo4j.core.ReactiveDatabaseSelectionProvider; @@ -30,6 +30,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -49,55 +51,66 @@ public abstract class AbstractReactiveTestBase { TestEntityWithGeneratedDeprecatedId2 t2 = new TestEntityWithGeneratedDeprecatedId2(null, "v2"); TestEntityWithGeneratedDeprecatedId1 t1 = new TestEntityWithGeneratedDeprecatedId1(null, "v1", t2); - TestEntityWithGeneratedDeprecatedId1 result = generatedDeprecatedIdRepository.save(t1).block(); + TestEntityWithGeneratedDeprecatedId1 result = this.generatedDeprecatedIdRepository.save(t1).block(); - TestEntityWithGeneratedDeprecatedId1 freshRetrieved = generatedDeprecatedIdRepository.findById(result.getId()).block(); + TestEntityWithGeneratedDeprecatedId1 freshRetrieved = this.generatedDeprecatedIdRepository + .findById(result.getId()) + .block(); - Assertions.assertNotNull(result.getRelatedEntity()); - Assertions.assertNotNull(freshRetrieved.getRelatedEntity()); + assertThat(result.getRelatedEntity()).isNotNull(); + assertThat(freshRetrieved.getRelatedEntity()).isNotNull(); } /** - * This is a test to ensure if the fix for the failing test above will continue to work for - * assigned ids. For broader test cases please return false for isCypher5Compatible in (Reactive)RepositoryIT + * This is a test to ensure if the fix for the failing test above will continue to + * work for assigned ids. For broader test cases please return false for + * isCypher5Compatible in (Reactive)RepositoryIT */ @Test public void testAssignedIds() { TestEntityWithAssignedId2 t2 = new TestEntityWithAssignedId2("second", "v2"); TestEntityWithAssignedId1 t1 = new TestEntityWithAssignedId1("first", "v1", t2); - TestEntityWithAssignedId1 result = assignedIdRepository.save(t1).block(); + TestEntityWithAssignedId1 result = this.assignedIdRepository.save(t1).block(); - TestEntityWithAssignedId1 freshRetrieved = assignedIdRepository.findById(result.getAssignedId()).block(); + TestEntityWithAssignedId1 freshRetrieved = this.assignedIdRepository.findById(result.getAssignedId()).block(); - Assertions.assertNotNull(result.getRelatedEntity()); - Assertions.assertNotNull(freshRetrieved.getRelatedEntity()); + assertThat(result.getRelatedEntity()).isNotNull(); + assertThat(freshRetrieved.getRelatedEntity()).isNotNull(); } - interface TestEntityWithGeneratedDeprecatedId1Repository extends ReactiveNeo4jRepository { + interface TestEntityWithGeneratedDeprecatedId1Repository + extends ReactiveNeo4jRepository { + } interface TestEntityWithAssignedId1Repository extends ReactiveNeo4jRepository { + } abstract static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractTestBase.java index 8850f2c14..41486a6c4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/AbstractTestBase.java @@ -15,9 +15,9 @@ */ package org.springframework.data.neo4j.integration.issues.gh2728; -import org.junit.jupiter.api.Assertions; 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.data.neo4j.core.DatabaseSelectionProvider; @@ -30,6 +30,8 @@ import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier */ @@ -49,56 +51,65 @@ public abstract class AbstractTestBase { TestEntityWithGeneratedDeprecatedId2 t2 = new TestEntityWithGeneratedDeprecatedId2(null, "v2"); TestEntityWithGeneratedDeprecatedId1 t1 = new TestEntityWithGeneratedDeprecatedId1(null, "v1", t2); - TestEntityWithGeneratedDeprecatedId1 result = generatedDeprecatedIdRepository.save(t1); + TestEntityWithGeneratedDeprecatedId1 result = this.generatedDeprecatedIdRepository.save(t1); - TestEntityWithGeneratedDeprecatedId1 freshRetrieved = generatedDeprecatedIdRepository.findById(result.getId()).get(); + TestEntityWithGeneratedDeprecatedId1 freshRetrieved = this.generatedDeprecatedIdRepository + .findById(result.getId()) + .get(); - Assertions.assertNotNull(result.getRelatedEntity()); - Assertions.assertNotNull(freshRetrieved.getRelatedEntity()); + assertThat(result.getRelatedEntity()).isNotNull(); + assertThat(freshRetrieved.getRelatedEntity()).isNotNull(); } /** - * This is a test to ensure if the fix for the failing test above will continue to work for - * assigned ids. For broader test cases please return false for isCypher5Compatible in (Reactive)RepositoryIT + * This is a test to ensure if the fix for the failing test above will continue to + * work for assigned ids. For broader test cases please return false for + * isCypher5Compatible in (Reactive)RepositoryIT */ @Test public void testAssignedIds() { TestEntityWithAssignedId2 t2 = new TestEntityWithAssignedId2("second", "v2"); TestEntityWithAssignedId1 t1 = new TestEntityWithAssignedId1("first", "v1", t2); - TestEntityWithAssignedId1 result = assignedIdRepository.save(t1); + TestEntityWithAssignedId1 result = this.assignedIdRepository.save(t1); - TestEntityWithAssignedId1 freshRetrieved = assignedIdRepository.findById(result.getAssignedId()).get(); + TestEntityWithAssignedId1 freshRetrieved = this.assignedIdRepository.findById(result.getAssignedId()).get(); - Assertions.assertNotNull(result.getRelatedEntity()); - Assertions.assertNotNull(freshRetrieved.getRelatedEntity()); + assertThat(result.getRelatedEntity()).isNotNull(); + assertThat(freshRetrieved.getRelatedEntity()).isNotNull(); } - interface TestEntityWithGeneratedDeprecatedId1Repository extends Neo4jRepository { + interface TestEntityWithGeneratedDeprecatedId1Repository + extends Neo4jRepository { + } interface TestEntityWithAssignedId1Repository extends Neo4jRepository { + } abstract static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override public PlatformTransactionManager transactionManager(Driver driver, - DatabaseSelectionProvider databaseNameProvider) { + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectConfigIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectConfigIT.java index 83a1a66ff..c8d9e6534 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectConfigIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectConfigIT.java @@ -30,9 +30,12 @@ public class CorrectConfigIT extends AbstractTestBase { @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends AbstractTestBase.Config { + @Override public boolean isCypher5Compatible() { return AbstractTestBase.neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectReactiveConfigIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectReactiveConfigIT.java index 5afdd34dc..b3fbdbd34 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectReactiveConfigIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/CorrectReactiveConfigIT.java @@ -30,9 +30,12 @@ public class CorrectReactiveConfigIT extends AbstractReactiveTestBase { @EnableTransactionManagement @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) static class Config extends AbstractReactiveTestBase.Config { + @Override public boolean isCypher5Compatible() { return AbstractReactiveTestBase.neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId1.java index d2bd164ea..f7ed3d829 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId1.java @@ -42,10 +42,11 @@ public class TestEntityWithAssignedId1 { } public String getAssignedId() { - return assignedId; + return this.assignedId; } public TestEntityWithAssignedId2 getRelatedEntity() { - return relatedEntity; + return this.relatedEntity; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId2.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId2.java index 889bd61cb..aec645060 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithAssignedId2.java @@ -35,4 +35,5 @@ public class TestEntityWithAssignedId2 { this.assignedId = assignedId; this.valueTwo = valueTwo; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId1.java index 6d6d99ef2..bc62015ed 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId1.java @@ -37,17 +37,19 @@ public class TestEntityWithGeneratedDeprecatedId1 { @Relationship("related_to") private TestEntityWithGeneratedDeprecatedId2 relatedEntity; - public TestEntityWithGeneratedDeprecatedId1(Long id, String valueOne, TestEntityWithGeneratedDeprecatedId2 relatedEntity) { + public TestEntityWithGeneratedDeprecatedId1(Long id, String valueOne, + TestEntityWithGeneratedDeprecatedId2 relatedEntity) { this.id = id; this.valueOne = valueOne; this.relatedEntity = relatedEntity; } public Long getId() { - return id; + return this.id; } public TestEntityWithGeneratedDeprecatedId2 getRelatedEntity() { - return relatedEntity; + return this.relatedEntity; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId2.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId2.java index c76e755d0..668587858 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/TestEntityWithGeneratedDeprecatedId2.java @@ -37,4 +37,5 @@ public class TestEntityWithGeneratedDeprecatedId2 { this.id = id; this.valueTwo = valueTwo; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongConfigIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongConfigIT.java index db50b0052..de7ac4da7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongConfigIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongConfigIT.java @@ -30,11 +30,14 @@ public class WrongConfigIT extends AbstractTestBase { @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends AbstractTestBase.Config { + @Override public boolean isCypher5Compatible() { // explicitly not compatible with Neo4j 5 although connected to one // same as default Cypher-DSL configuration / dialect return false; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongReactiveConfigIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongReactiveConfigIT.java index aff8ee722..08c42c732 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongReactiveConfigIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2728/WrongReactiveConfigIT.java @@ -30,11 +30,14 @@ public class WrongReactiveConfigIT extends AbstractReactiveTestBase { @EnableTransactionManagement @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) static class Config extends AbstractReactiveTestBase.Config { + @Override public boolean isCypher5Compatible() { // explicitly not compatible with Neo4j 5 although connected to one // same as default Cypher-DSL configuration / dialect return false; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java index 99d0cfa52..da64cd1a6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java @@ -28,23 +28,31 @@ public class GH2819Model { * Projection of ParentA/ChildA */ public interface ChildAProjection { + String getName(); + GH2819Model.ChildBProjection getParentB(); + } /** * Projection of ParentB/ChildB */ public interface ChildBProjection { + String getName(); + GH2819Model.ChildCProjection getParentC(); + } /** * Projection of ParentC/ChildC */ public interface ChildCProjection { + String getName(); + } /** @@ -52,7 +60,9 @@ public class GH2819Model { */ @Node public static class ParentA { - @Id public String id; + + @Id + public String id; @Relationship(type = "HasBs", direction = Relationship.Direction.OUTGOING) public ParentB parentB; @@ -60,12 +70,13 @@ public class GH2819Model { public String name; public String getName() { - return name; + return this.name; } public ParentB getParentB() { - return parentB; + return this.parentB; } + } /** @@ -73,7 +84,9 @@ public class GH2819Model { */ @Node public static class ParentB { - @Id public String id; + + @Id + public String id; @Relationship(type = "HasCs", direction = Relationship.Direction.OUTGOING) public ParentC parentC; @@ -81,12 +94,13 @@ public class GH2819Model { public String name; public ParentC getParentC() { - return parentC; + return this.parentC; } public String getName() { - return name; + return this.name; } + } /** @@ -94,13 +108,16 @@ public class GH2819Model { */ @Node public static class ParentC { - @Id public String id; + + @Id + public String id; public String name; public String getName() { - return name; + return this.name; } + } /** @@ -126,4 +143,5 @@ public class GH2819Model { public static class ChildC extends ParentC { } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java index 0a85d8bf0..945c139e3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java @@ -23,4 +23,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; public interface GH2819Repository extends Neo4jRepository { GH2819Model.ChildAProjection findById(String id, Class projectionClass); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858.java index 52adda47c..6ec427087 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858.java @@ -15,13 +15,13 @@ */ package org.springframework.data.neo4j.integration.issues.gh2858; +import java.util.List; + 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.Relationship; -import java.util.List; - /** * @author Gerrit Meier */ @@ -44,24 +44,33 @@ public class GH2858 { * Projection of GH2858 entity */ public interface GH2858Projection { + String getName(); + List getFriends(); + List getRelatives(); /** * Additional projection with just the name field. */ interface KnownPerson { + String getName(); + } /** * Additional projection with name field and friends relationship. */ interface Friend { + String getName(); + List getFriends(); + } + } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858Repository.java index 1be5bd73b..a1f3d0e8b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2858/GH2858Repository.java @@ -23,4 +23,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; public interface GH2858Repository extends Neo4jRepository { GH2858.GH2858Projection findOneByName(String name); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Apple.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Apple.java index 417fe8589..543c2b0c5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Apple.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Apple.java @@ -22,4 +22,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node(primaryLabel = "Apple") public class Apple extends MagicalFruit { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Fruit.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Fruit.java index 758421103..94166cf32 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Fruit.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Fruit.java @@ -37,15 +37,16 @@ public abstract class Fruit { return this.id; } - public Set getLabels() { - return this.labels; - } - public void setId(String id) { this.id = id; } + public Set getLabels() { + return this.labels; + } + public void setLabels(Set labels) { this.labels = labels; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/FruitRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/FruitRepository.java index 972b411df..25b880025 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/FruitRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/FruitRepository.java @@ -26,6 +26,8 @@ import org.springframework.stereotype.Repository; */ @Repository public interface FruitRepository extends Neo4jRepository { + @Query("MATCH (f:Fruit) RETURN f") List findAllFruits(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/MagicalFruit.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/MagicalFruit.java index 4206fb469..71db0c9c3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/MagicalFruit.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/MagicalFruit.java @@ -31,15 +31,16 @@ public class MagicalFruit extends Fruit { return this.volume; } - public String getColor() { - return this.color; - } - public void setVolume(double volume) { this.volume = volume; } + public String getColor() { + return this.color; + } + public void setColor(String color) { this.color = color; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Orange.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Orange.java index 121c370f0..08798e5b2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Orange.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2886/Orange.java @@ -22,4 +22,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node(primaryLabel = "Orange") public class Orange extends MagicalFruit { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java index e533f6ea5..7cb571cd9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java @@ -25,6 +25,7 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version public class BugFromV1 { + @Id @GeneratedValue(UUIDStringGenerator.class) protected String uuid; @@ -51,8 +52,11 @@ public class BugFromV1 { * Lombok builder */ public static class BugFromBuilder { + private String uuid; + private String name; + private BugRelationshipV1 reli; BugFromBuilder() { @@ -81,5 +85,7 @@ public class BugFromV1 { public String toString() { return "BugFrom.BugFromBuilder(uuid=" + this.uuid + ", name=" + this.name + ", reli=" + this.reli + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java index bf351431c..996ea40d4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java @@ -25,6 +25,7 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version @RelationshipProperties public class BugRelationshipV1 { + @RelationshipId protected Long id; @@ -47,8 +48,11 @@ public class BugRelationshipV1 { * Lombok builder */ public static class BugRelationshipBuilder { + private Long id; + private String comment; + private BugTargetBaseV1 target; BugRelationshipBuilder() { @@ -73,8 +77,12 @@ public class BugRelationshipV1 { return new BugRelationshipV1(this.id, this.comment, this.target); } + @Override public String toString() { - return "BugRelationship.BugRelationshipBuilder(id=" + this.id + ", comment=" + this.comment + ", target=" + this.target + ")"; + return "BugRelationship.BugRelationshipBuilder(id=" + this.id + ", comment=" + this.comment + ", target=" + + this.target + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java index c91be69c7..573fa1f7f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java @@ -28,18 +28,20 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @Node public abstract class BugTargetBaseV1 { + + @Relationship(type = "RELI", direction = Relationship.Direction.OUTGOING) + public Set relatedBugs; + @Id @GeneratedValue(UUIDStringGenerator.class) protected String uuid; private String name; - @Relationship(type = "RELI", direction = Relationship.Direction.OUTGOING) - public Set relatedBugs; - BugTargetBaseV1(String uuid, String name, Set relatedBugs) { this.uuid = uuid; this.name = name; this.relatedBugs = relatedBugs; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java index de1d0d4ea..0b6514432 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java @@ -22,6 +22,7 @@ import java.util.Set; */ @SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version public class BugTargetV1 extends BugTargetBaseV1 { + private String type; BugTargetV1(String uuid, String name, Set relatedBugs, String type) { @@ -37,9 +38,13 @@ public class BugTargetV1 extends BugTargetBaseV1 { * Builder */ public static class BugTargetBuilder { + private String uuid; + private String name; + private Set relatedBugs; + private String type; BugTargetBuilder() { @@ -69,8 +74,12 @@ public class BugTargetV1 extends BugTargetBaseV1 { return new BugTargetV1(this.uuid, this.name, this.relatedBugs, this.type); } + @Override public String toString() { - return "BugTarget.BugTargetBuilder(uuid=" + this.uuid + ", name=" + this.name + ", relatedBugs=" + this.relatedBugs + ", type=" + this.type + ")"; + return "BugTarget.BugTargetBuilder(uuid=" + this.uuid + ", name=" + this.name + ", relatedBugs=" + + this.relatedBugs + ", type=" + this.type + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java index e8bc27ef1..6ac074252 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface FromRepositoryV1 extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java index 744bdea3d..88bcfbd39 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Gerrit Meier */ public interface ReactiveFromRepositoryV1 extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java index aeb284043..c40edb719 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Gerrit Meier */ public interface ReactiveToRepositoryV1 extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java index 4506e5786..b6f0aadfe 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface ToRepositoryV1 extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java index ea46ce300..a2341a2a8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java @@ -27,16 +27,18 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @Node public class BugFrom { + @Id @GeneratedValue(UUIDStringGenerator.class) public String uuid; - String name; - @Relationship(type = "RELI", direction = Relationship.Direction.INCOMING) public IncomingBugRelationship reli; - @PersistenceCreator // Due to the cyclic mapping you cannot have the relation as constructor parameter, how should this work? + String name; + + @PersistenceCreator // Due to the cyclic mapping you cannot have the relation as + // constructor parameter, how should this work? BugFrom(String name, String uuid) { this.name = name; this.uuid = uuid; @@ -48,10 +50,9 @@ public class BugFrom { this.reli = new IncomingBugRelationship(comment, target); } - @Override public String toString() { - return String.format(" {uuid: %s, name: %s}", uuid, name); + return String.format(" {uuid: %s, name: %s}", this.uuid, this.name); } -} +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java index 9dfb852a1..17490b376 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java @@ -20,11 +20,11 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; /** - * @author Mathias Kühn * @param The crux of this thing + * @author Mathias Kühn */ @RelationshipProperties -public abstract class BugRelationship { +public abstract class BugRelationship { @RelationshipId public Long id; @@ -41,7 +41,7 @@ public abstract class BugRelationship { @Override public String toString() { - return String.format("<%s> {id: %d, comment: %s}", this.getClass().getSimpleName(), id, comment); + return String.format("<%s> {id: %d, comment: %s}", this.getClass().getSimpleName(), this.id, this.comment); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java index de7b3a1b3..a1884cb4c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java @@ -30,4 +30,5 @@ public class BugTarget extends BugTargetBase { this.type = type; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java index 2a9b86e4f..cd502724b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java @@ -45,7 +45,7 @@ public abstract class BugTargetBase { @Override public String toString() { - return String.format("<%s> {uuid: %s, name: %s}", this.getClass().getSimpleName(), uuid, name); + return String.format("<%s> {uuid: %s, name: %s}", this.getClass().getSimpleName(), this.uuid, this.name); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java index 41eba9200..214866ac6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java @@ -33,4 +33,5 @@ public class BugTargetContainer extends BugTargetBase { public BugTargetContainer(String name) { super(name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java index fd4c5eb75..9660cab55 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface FromRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java index c322a6fa6..a8ebe6e7a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java @@ -23,4 +23,5 @@ public class IncomingBugRelationship extends BugRelationship { IncomingBugRelationship(String comment, BugTargetBase target) { super(comment, target); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java index c74781ebc..5b1808f42 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java @@ -23,4 +23,5 @@ public class OutgoingBugRelationship extends BugRelationship { public OutgoingBugRelationship(String comment, BugFrom target) { super(comment, target); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java index 5299eeb15..a6e462517 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Gerrit Meier */ public interface ReactiveFromRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java index d1b5f0041..69e7c8675 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Gerrit Meier */ public interface ReactiveToRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java index 93f0e4cb1..464d1af12 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Gerrit Meier */ public interface ToRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlace.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlace.java index 85dd63375..f0b9ee388 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlace.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlace.java @@ -27,4 +27,5 @@ public interface HasNameAndPlace { String getName(); Point getPlace(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlaceRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlaceRepository.java index 171a92f15..d1df473c1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlaceRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/HasNameAndPlaceRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.integration.issues.gh2908; import org.neo4j.driver.types.Point; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; @@ -24,9 +25,11 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.neo4j.repository.Neo4jRepository; /** - * Just extending {@link Neo4jRepository} here did not work, so it's split in the concrete interface. + * Just extending {@link Neo4jRepository} here did not work, so it's split in the concrete + * interface. + * + * @param concrete type of the entity * @author Michael J. Simons - * @param Concrete type of the entity */ public interface HasNameAndPlaceRepository { @@ -37,4 +40,5 @@ public interface HasNameAndPlaceRepository { GeoResults findAllByPlaceNear(Point p, Range between); GeoPage findAllByPlaceNear(Point p, Range between, Pageable pageable); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNode.java index 19a60bc85..6b22c6def 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNode.java @@ -16,41 +16,44 @@ package org.springframework.data.neo4j.integration.issues.gh2908; import org.neo4j.driver.types.Point; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; /** * A located node without circles. + * * @author Michael J. Simons */ @Node public class LocatedNode implements HasNameAndPlace { - @Id - @GeneratedValue - private String id; - private final String name; private final Point place; + @Id + @GeneratedValue + private String id; + public LocatedNode(String name, Point place) { this.name = name; this.place = place; } public String getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override public Point getPlace() { - return place; + return this.place; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeRepository.java index b993f5f5a..4305d770d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeRepository.java @@ -21,9 +21,12 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; /** * Repository spotting all supported Geo* results. + * * @author Michael J. Simons */ -public interface LocatedNodeRepository extends HasNameAndPlaceRepository, Neo4jRepository { +public interface LocatedNodeRepository + extends HasNameAndPlaceRepository, Neo4jRepository { Page findAllByName(String whatever, PageRequest name); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRef.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRef.java index eff709f48..7a7d2c8a8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRef.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRef.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.integration.issues.gh2908; import org.neo4j.driver.types.Point; + import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -23,19 +24,20 @@ import org.springframework.data.neo4j.core.schema.Relationship; /** * A located node with circles. + * * @author Michael J. Simons */ @Node public class LocatedNodeWithSelfRef implements HasNameAndPlace { - @Id - @GeneratedValue - private String id; - private final String name; private final Point place; + @Id + @GeneratedValue + private String id; + @Relationship private LocatedNodeWithSelfRef next; @@ -45,24 +47,25 @@ public class LocatedNodeWithSelfRef implements HasNameAndPlace { } public String getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override public Point getPlace() { - return place; + return this.place; } public LocatedNodeWithSelfRef getNext() { - return next; + return this.next; } public void setNext(LocatedNodeWithSelfRef next) { this.next = next; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRefRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRefRepository.java index 07257778b..8df1783f3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRefRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/LocatedNodeWithSelfRefRepository.java @@ -19,7 +19,10 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; /** * Repository spotting all supported Geo* results. + * * @author Michael J. Simons */ -public interface LocatedNodeWithSelfRefRepository extends HasNameAndPlaceRepository, Neo4jRepository { +public interface LocatedNodeWithSelfRefRepository + extends HasNameAndPlaceRepository, Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/Place.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/Place.java index 7f7cdc690..f431e74a4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/Place.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/Place.java @@ -20,6 +20,7 @@ import org.neo4j.driver.types.Point; /** * Cool places to be. + * * @author Michael J. Simons */ public enum Place { @@ -36,6 +37,7 @@ public enum Place { } public Point getValue() { - return value; + return this.value; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/ReactiveLocatedNodeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/ReactiveLocatedNodeRepository.java index 0fb15ac20..c4caba102 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/ReactiveLocatedNodeRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2908/ReactiveLocatedNodeRepository.java @@ -16,15 +16,16 @@ package org.springframework.data.neo4j.integration.issues.gh2908; import org.neo4j.driver.types.Point; +import reactor.core.publisher.Flux; + import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; -import reactor.core.publisher.Flux; - /** * Repository spotting all supported Geo* results. + * * @author Michael J. Simons */ public interface ReactiveLocatedNodeRepository extends ReactiveNeo4jRepository { @@ -34,4 +35,5 @@ public interface ReactiveLocatedNodeRepository extends ReactiveNeo4jRepository> findAllByPlaceNear(Point p, Distance max); Flux> findAllByPlaceNear(Point p, Range between); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionNode.java index 624886fc1..7ee9e8cc2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionNode.java @@ -28,6 +28,7 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @Node public class ConditionNode { + @Id @GeneratedValue(UUIDStringGenerator.class) public String uuid; @@ -37,4 +38,5 @@ public class ConditionNode { @Relationship(type = "CAUSES", direction = Relationship.Direction.OUTGOING) public Set downstreamFailures; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionRepository.java index f6de71626..fca7f92ea 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/ConditionRepository.java @@ -21,4 +21,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Mathias Kühn */ public interface ConditionRepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureNode.java index 42c6a4c6f..bc11732a1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureNode.java @@ -25,7 +25,9 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @Node public class FailureNode { + @Id @GeneratedValue(UUIDStringGenerator.class) public String uuid; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureRelationship.java index 04fcedd4a..3846c1831 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2918/FailureRelationship.java @@ -30,4 +30,5 @@ public abstract class FailureRelationship { @TargetNode public FailureNode target; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyModel.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyModel.java index 2e6278378..2632ae002 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyModel.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyModel.java @@ -27,17 +27,18 @@ import org.springframework.data.neo4j.core.support.UUIDStringGenerator; */ @Node public class MyModel { + @Id @GeneratedValue(generatorClass = UUIDStringGenerator.class) private String uuid; private String name; - @Relationship(value = "REL_TO_MY_NESTED_MODEL") + @Relationship("REL_TO_MY_NESTED_MODEL") private MyModel myNestedModel; public MyModel getMyNestedModel() { - return myNestedModel; + return this.myNestedModel; } public void setMyNestedModel(MyModel myNestedModel) { @@ -45,7 +46,7 @@ public class MyModel { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -53,10 +54,11 @@ public class MyModel { } public String getUuid() { - return uuid; + return this.uuid; } public void setUuid(String uuid) { this.uuid = uuid; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyRepository.java index 51be2a8c2..5635d66a9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2963/MyRepository.java @@ -26,23 +26,24 @@ import org.springframework.data.repository.CrudRepository; */ public interface MyRepository extends CrudRepository { - @Query(""" - MATCH (root:MyModel {uuid: $uuid}) - RETURN root { - .*, MyModel_REL_TO_MY_NESTED_MODEL_MyModel: [ - (root)-[:REL_TO_MY_NESTED_MODEL]->(nested:MyModel) | nested {. *} - ] - } - """) - Optional getByUuidCustomQuery(String uuid); + @Query(""" + MATCH (root:MyModel {uuid: $uuid}) + RETURN root { + .*, MyModel_REL_TO_MY_NESTED_MODEL_MyModel: [ + (root)-[:REL_TO_MY_NESTED_MODEL]->(nested:MyModel) | nested {. *} + ] + } + """) + Optional getByUuidCustomQuery(String uuid); @Query(""" - MATCH (root:MyModel {uuid: $uuid}) - RETURN root { - .*, MyModel_REL_TO_MY_NESTED_MODEL_MyModel_true: [ - (root)-[:REL_TO_MY_NESTED_MODEL]->(nested:MyModel) | nested {. *} - ] - } - """) + MATCH (root:MyModel {uuid: $uuid}) + RETURN root { + .*, MyModel_REL_TO_MY_NESTED_MODEL_MyModel_true: [ + (root)-[:REL_TO_MY_NESTED_MODEL]->(nested:MyModel) | nested {. *} + ] + } + """) Optional getByUuidCustomQueryV2(String uuid); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java index 91ac2899e..fbc0129f1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java @@ -15,30 +15,31 @@ */ package org.springframework.data.neo4j.integration.issues.gh2973; -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.Relationship; - import java.util.HashMap; import java.util.List; import java.util.Map; 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.Relationship; /** * @author yangyaofei */ @Node public class BaseNode { + @Id @GeneratedValue private UUID id; + @Relationship(direction = Relationship.Direction.OUTGOING) private Map> relationships = new HashMap<>(); public UUID getId() { - return id; + return this.id; } public void setId(UUID id) { @@ -46,10 +47,11 @@ public class BaseNode { } public Map> getRelationships() { - return relationships; + return this.relationships; } public void setRelationships(Map> relationships) { this.relationships = relationships; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java index b3d3bb153..81dca88b3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java @@ -25,14 +25,16 @@ import org.springframework.data.neo4j.core.schema.TargetNode; */ @RelationshipProperties public abstract class BaseRelationship { + @RelationshipId @GeneratedValue private Long id; + @TargetNode private BaseNode targetNode; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -40,10 +42,11 @@ public abstract class BaseRelationship { } public BaseNode getTargetNode() { - return targetNode; + return this.targetNode; } public void setTargetNode(BaseNode targetNode) { this.targetNode = targetNode; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java index 42a03cfee..b75b49db4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java @@ -15,14 +15,15 @@ */ package org.springframework.data.neo4j.integration.issues.gh2973; -import org.springframework.data.neo4j.repository.Neo4jRepository; - import java.util.UUID; +import org.springframework.data.neo4j.repository.Neo4jRepository; + /** * Test repository for GH-2973 * * @author yangyaofei */ public interface Gh2973Repository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java index 22b026e9c..299c81c64 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java @@ -22,13 +22,15 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; */ @RelationshipProperties(persistTypeInfo = true) public class RelationshipA extends BaseRelationship { + String a; public String getA() { - return a; + return this.a; } public void setA(String a) { this.a = a; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java index 1243a159a..b6a7f293d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java @@ -22,13 +22,15 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; */ @RelationshipProperties(persistTypeInfo = true) public class RelationshipB extends BaseRelationship { + String b; public String getB() { - return b; + return this.b; } public void setB(String b) { this.b = b; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java index 9172778f3..8bbb4c4a8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java @@ -22,13 +22,15 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; */ @RelationshipProperties public class RelationshipC extends BaseRelationship { + String c; public String getC() { - return c; + return this.c; } public void setC(String c) { this.c = c; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java index 480ad6114..5b61ab920 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java @@ -22,13 +22,15 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; */ @RelationshipProperties public class RelationshipD extends BaseRelationship { + String d; public String getD() { - return d; + return this.d; } public void setD(String d) { this.d = d; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/NestedProjectionsIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/NestedProjectionsIT.java index 739515001..cadccd0c5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/NestedProjectionsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/NestedProjectionsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues.projections; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.Optional; @@ -26,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -44,6 +43,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -56,7 +57,9 @@ class NestedProjectionsIT { static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { try (Session session = driver.session()) { session.run("MATCH (n) DETACH DELETE n").consume(); - session.run("CREATE (l:SourceNodeA {id: 'L-l1', version: 1})-[:A_TO_CENTRAL]->(e:CentralNode {id: 'E-l1', version: 1})<-[:B_TO_CENTRAL]-(c:SourceNodeB {id: 'C-l1', version: 1}) RETURN id(l)").consume(); + session.run( + "CREATE (l:SourceNodeA {id: 'L-l1', version: 1})-[:A_TO_CENTRAL]->(e:CentralNode {id: 'E-l1', version: 1})<-[:B_TO_CENTRAL]-(c:SourceNodeB {id: 'C-l1', version: 1}) RETURN id(l)") + .consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -71,7 +74,7 @@ class NestedProjectionsIT { } @RepeatedTest(20) - // GH-2581 + // GH-2581 void excludedHopMustNotVanish(@Autowired SourceNodeARepository repository) { Optional optionalSourceNode = repository.findById("L-l1"); @@ -82,7 +85,6 @@ class NestedProjectionsIT { toUpdate.getCentralNode().setName("whatever"); SourceNodeAProjection projectedSourceNode = repository.saveWithProjection(toUpdate); - SourceNodeA updatedNode = repository.findById("L-l1").orElseThrow(IllegalStateException::new); assertThat(updatedNode.getCentralNode()).isNotNull(); @@ -102,13 +104,13 @@ class NestedProjectionsIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager( - Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -121,6 +123,7 @@ class NestedProjectionsIT { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -130,5 +133,7 @@ class NestedProjectionsIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/CentralNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/CentralNode.java index f843f2cc3..37c477499 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/CentralNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/CentralNode.java @@ -15,14 +15,14 @@ */ package org.springframework.data.neo4j.integration.issues.projections.model; +import java.util.Objects; + import org.springframework.data.annotation.Version; 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; -import java.util.Objects; - /** * @author Michael J. Simons */ @@ -30,13 +30,13 @@ import java.util.Objects; @Node public class CentralNode { + @Version + Long version; + @Id @Property(name = "id") private String id; - @Version - Long version; - private String name; @Relationship(value = "B_TO_CENTRAL", direction = Relationship.Direction.INCOMING) @@ -56,31 +56,6 @@ public class CentralNode { return new CentralNodeBuilder(); } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CentralNode centralNode = (CentralNode) o; - return Objects.equals(id, centralNode.id) && Objects.equals(name, centralNode.name); - } - - @Override - public int hashCode() { - return Objects.hash(id, name); - } - - public void setSourceNodeB(SourceNodeB sourceNodeB) { - this.sourceNodeB = sourceNodeB; - } - - public void setName(String name) { - this.name = name; - } - public String getId() { return this.id; } @@ -93,17 +68,46 @@ public class CentralNode { return this.name; } + public void setName(String name) { + this.name = name; + } + public SourceNodeB getSourceNodeB() { return this.sourceNodeB; } + public void setSourceNodeB(SourceNodeB sourceNodeB) { + this.sourceNodeB = sourceNodeB; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CentralNode centralNode = (CentralNode) o; + return Objects.equals(this.id, centralNode.id) && Objects.equals(this.name, centralNode.name); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + /** * the builder */ public static class CentralNodeBuilder { + private String id; + private Long version; + private String name; + private SourceNodeB sourceNodeB; CentralNodeBuilder() { @@ -133,8 +137,12 @@ public class CentralNode { return new CentralNode(this.id, this.version, this.name, this.sourceNodeB); } + @Override public String toString() { - return "CentralNode.CentralNodeBuilder(id=" + this.id + ", version=" + this.version + ", name=" + this.name + ", sourceNodeB=" + this.sourceNodeB + ")"; + return "CentralNode.CentralNodeBuilder(id=" + this.id + ", version=" + this.version + ", name=" + this.name + + ", sourceNodeB=" + this.sourceNodeB + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeA.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeA.java index 8e5f909db..5830176d9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeA.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeA.java @@ -15,13 +15,13 @@ */ package org.springframework.data.neo4j.integration.issues.projections.model; +import java.util.Objects; + import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.Objects; - /** * @author Michael J. Simons */ @@ -29,12 +29,12 @@ import java.util.Objects; @Node public class SourceNodeA { - @Id - private String id; - @Version Long version; + @Id + private String id; + private String value; @Relationship("A_TO_CENTRAL") @@ -54,27 +54,6 @@ public class SourceNodeA { return new SourceNodeABuilder(); } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SourceNodeA sourceNodeA = (SourceNodeA) o; - return Objects.equals(id, sourceNodeA.id) && Objects.equals(value, sourceNodeA.value); - } - - @Override - public int hashCode() { - return Objects.hash(id, value); - } - - public void setValue(String value) { - this.value = value; - } - public String getId() { return this.id; } @@ -87,17 +66,42 @@ public class SourceNodeA { return this.value; } + public void setValue(String value) { + this.value = value; + } + public CentralNode getCentralNode() { return this.centralNode; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SourceNodeA sourceNodeA = (SourceNodeA) o; + return Objects.equals(this.id, sourceNodeA.id) && Objects.equals(this.value, sourceNodeA.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.value); + } + /** * the builder */ public static class SourceNodeABuilder { + private String id; + private Long version; + private String value; + private CentralNode centralNode; SourceNodeABuilder() { @@ -127,8 +131,12 @@ public class SourceNodeA { return new SourceNodeA(this.id, this.version, this.value, this.centralNode); } + @Override public String toString() { - return "SourceNodeA.SourceNodeABuilder(id=" + this.id + ", version=" + this.version + ", value=" + this.value + ", centralNode=" + this.centralNode + ")"; + return "SourceNodeA.SourceNodeABuilder(id=" + this.id + ", version=" + this.version + ", value=" + + this.value + ", centralNode=" + this.centralNode + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeB.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeB.java index e541e42cc..d3846b76f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeB.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/model/SourceNodeB.java @@ -15,16 +15,17 @@ */ package org.springframework.data.neo4j.integration.issues.projections.model; +import java.util.List; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonIgnore; + import org.springframework.data.annotation.Version; 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; -import java.util.List; -import java.util.Objects; - /** * @author Michael J. Simons */ @@ -32,13 +33,13 @@ import java.util.Objects; @Node public class SourceNodeB { + @Version + Long version; + @Id @Property(name = "id") private String id; - @Version - Long version; - private String name; @JsonIgnore @@ -59,23 +60,6 @@ public class SourceNodeB { return new SourceNodeBBuilder(); } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SourceNodeB sourceNodeB = (SourceNodeB) o; - return Objects.equals(id, sourceNodeB.id) && Objects.equals(name, sourceNodeB.name); - } - - @Override - public int hashCode() { - return Objects.hash(id, name); - } - public String getId() { return this.id; } @@ -92,13 +76,34 @@ public class SourceNodeB { return this.centralNodes; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SourceNodeB sourceNodeB = (SourceNodeB) o; + return Objects.equals(this.id, sourceNodeB.id) && Objects.equals(this.name, sourceNodeB.name); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + /** * the builder */ public static class SourceNodeBBuilder { + private String id; + private Long version; + private String name; + private List centralNodes; SourceNodeBBuilder() { @@ -129,8 +134,12 @@ public class SourceNodeB { return new SourceNodeB(this.id, this.version, this.name, this.centralNodes); } + @Override public String toString() { - return "SourceNodeB.SourceNodeBBuilder(id=" + this.id + ", version=" + this.version + ", name=" + this.name + ", centralNodes=" + this.centralNodes + ")"; + return "SourceNodeB.SourceNodeBBuilder(id=" + this.id + ", version=" + this.version + ", name=" + this.name + + ", centralNodes=" + this.centralNodes + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/projection/SourceNodeAProjection.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/projection/SourceNodeAProjection.java index 3c76b9bc1..d6b5a790f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/projection/SourceNodeAProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/projection/SourceNodeAProjection.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.integration.issues.projections.projection * @author Michael J. Simons */ public interface SourceNodeAProjection { + String getValue(); CentralNode getCentralNode(); @@ -27,8 +28,11 @@ public interface SourceNodeAProjection { * Nested projection. */ interface CentralNode { + String getId(); String getName(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepository.java index 8edc0efaf..8c989a880 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepository.java @@ -22,5 +22,7 @@ import org.springframework.data.neo4j.integration.issues.projections.projection. * @author Michael J. Simons */ public interface CustomRepository { + SourceNodeAProjection saveWithProjection(SourceNodeA sourceNodeA); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepositoryImpl.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepositoryImpl.java index 52f282073..55a617342 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepositoryImpl.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/CustomRepositoryImpl.java @@ -32,6 +32,7 @@ class CustomRepositoryImpl implements CustomRepository { @Override public SourceNodeAProjection saveWithProjection(SourceNodeA sourceNodeA) { - return neo4jOperations.saveAs(sourceNodeA, SourceNodeAProjection.class); + return this.neo4jOperations.saveAs(sourceNodeA, SourceNodeAProjection.class); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/SourceNodeARepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/SourceNodeARepository.java index ffb41c628..c5cecf779 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/SourceNodeARepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/projections/repository/SourceNodeARepository.java @@ -24,4 +24,5 @@ import org.springframework.stereotype.Repository; */ @Repository public interface SourceNodeARepository extends Neo4jRepository, CustomRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java index 18bee6a3a..76032f385 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.LogbackCapture; @@ -35,10 +36,33 @@ import static org.assertj.core.api.Assertions.assertThat; @Tag(Neo4jExtension.NEEDS_VERSION_SUPPORTING_ELEMENT_ID) abstract class AbstractElementIdTestBase { + private static final Pattern NEO5J_ELEMENT_ID_PATTERN = Pattern.compile("\\d+:.+:\\d+"); + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + static String adaptQueryTo44IfNecessary(String query) { + if (!neo4jConnectionSupport.isCypher5SyntaxCompatible()) { + query = query.replaceAll("elementId\\((.+?)\\)", "toString(id($1))"); + } + return query; + } + + static void assertThatLogMessageDoNotIndicateIDUsage(LogbackCapture logbackCapture) { + List formattedMessages = logbackCapture.getFormattedMessages(); + assertThat(formattedMessages) + .noneMatch(s -> s.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning") + || s.contains("The query used a deprecated function. ('id' is no longer supported)") + || s.contains("The query used a deprecated function: `id`.") + || s.matches("(?s).*toString\\(id\\(.*")); // No deprecations are + // logged when deprecated + // function call is + // nested. Anzeige ist + // raus. + } + @BeforeEach - void setupData(LogbackCapture logbackCapture, @Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { + void setupData(LogbackCapture logbackCapture, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { logbackCapture.addLogger("org.springframework.data.neo4j.cypher.deprecation", Level.WARN); logbackCapture.addLogger("org.springframework.data.neo4j.cypher", Level.DEBUG); @@ -48,8 +72,6 @@ abstract class AbstractElementIdTestBase { } } - private static final Pattern NEO5J_ELEMENT_ID_PATTERN = Pattern.compile("\\d+:.+:\\d+"); - Predicate validIdForCurrentNeo4j() { Predicate nonNull = Objects::nonNull; @@ -61,26 +83,12 @@ abstract class AbstractElementIdTestBase { return nonNull.and(v -> { try { Long.parseLong(v); - } catch (NumberFormatException e) { + } + catch (NumberFormatException ex) { return false; } return true; }); } - static String adaptQueryTo44IfNecessary(String query) { - if (!neo4jConnectionSupport.isCypher5SyntaxCompatible()) { - query = query.replaceAll("elementId\\((.+?)\\)", "toString(id($1))"); - } - return query; - } - - static void assertThatLogMessageDoNotIndicateIDUsage(LogbackCapture logbackCapture) { - List formattedMessages = logbackCapture.getFormattedMessages(); - assertThat(formattedMessages) - .noneMatch(s -> s.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning") || - s.contains("The query used a deprecated function. ('id' is no longer supported)") || - s.contains("The query used a deprecated function: `id`.") || - s.matches("(?s).*toString\\(id\\(.*")); // No deprecations are logged when deprecated function call is nested. Anzeige ist raus. - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java index df041e247..12da88094 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ImperativeElementIdIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.issues.pure_element_id; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.Map; @@ -26,6 +24,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -43,8 +42,11 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** - * Assertions that no {@code id()} calls are generated when no deprecated id types are present + * Assertions that no {@code id()} calls are generated when no deprecated id types are + * present * * @author Michael J. Simons */ @@ -52,22 +54,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @ExtendWith(LogbackCapturingExtension.class) public class ImperativeElementIdIT extends AbstractElementIdTestBase { - interface Repo1 extends Neo4jRepository { - - NodeWithGeneratedId1 findByIdIn(List ids); - } - - interface Repo2 extends Neo4jRepository { - - NodeWithGeneratedId2 findByRelatedNodesIdIn(List ids); - } - - interface Repo3 extends Neo4jRepository { - } - - interface Repo4 extends Neo4jRepository { - } - @Test void dontCallIdForDerivedQueriesWithInClause(LogbackCapture logbackCapture, @Autowired Repo1 repo1) { @@ -97,8 +83,7 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { void simpleNodeCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) { var node = repo1.save(new NodeWithGeneratedId1("from-sdn-repo")); - assertThat(node.getId()) - .matches(validIdForCurrentNeo4j()); + assertThat(node.getId()).matches(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @@ -106,75 +91,86 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { void simpleNodeAllCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) { var nodes = repo1.saveAll(List.of(new NodeWithGeneratedId1("from-sdn-repo"))); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String id; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId(); + id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode() + .elementId(); } var optionalNode = repo1.findById(id); - assertThat(optionalNode).map(NodeWithGeneratedId1::getValue) - .hasValue("whatever"); + assertThat(optionalNode).map(NodeWithGeneratedId1::getValue).hasValue("whatever"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId(); + session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode() + .elementId(); } var nodes = repo1.findAll(); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { NodeWithGeneratedId1 node; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode(); + var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode(); node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited"); node.setId(dbNode.elementId()); } node = repo1.save(node); - assertThat(node).extracting(NodeWithGeneratedId1::getValue) - .isEqualTo("whatever_edited"); + assertThat(node).extracting(NodeWithGeneratedId1::getValue).isEqualTo("whatever_edited"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { NodeWithGeneratedId1 node; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode(); + var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode(); node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited"); node.setId(dbNode.elementId()); } var nodes = repo1.saveAll(List.of(node)); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, + @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId2("owner"); owner.setRelatedNodes(List.of(new NodeWithGeneratedId1("child1"), new NodeWithGeneratedId1("child2"))); @@ -182,28 +178,29 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { assertThat(owner.getId()).isNotNull(); assertThat(owner.getRelatedNodes()) - .allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j())); + .allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j())); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, + @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; String ownedId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var row = session.run("CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *").single(); + var row = session.run( + "CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *") + .single(); ownerId = row.get("n").asNode().elementId(); ownedId = row.get("m").asNode().elementId(); } var owner = repo2.findById(ownerId).orElseThrow(); - assertThat(owner.getRelatedNodes()) - .hasSize(1) - .first() - .extracting(NodeWithGeneratedId1::getId) - .isEqualTo(ownedId); - + assertThat(owner.getRelatedNodes()).hasSize(1) + .first() + .extracting(NodeWithGeneratedId1::getId) + .isEqualTo(ownedId); owner.getRelatedNodes().get(0).setValue("owned_changed"); owner.setValue("owner_changed"); @@ -213,58 +210,66 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) - WHERE elementId(n) = $id1 AND elementId(m) = $id2 - RETURN count(*)"""), - Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)).single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) + WHERE elementId(n) = $id1 AND elementId(m) = $id2 + RETURN count(*)"""), + Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)) + .single() + .get(0) + .asLong(); assertThat(count).isOne(); } } @Test - void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId3("owner"); var target1 = new NodeWithGeneratedId1("target1"); var target2 = new NodeWithGeneratedId1("target2"); - owner.setRelatedNodes( - List.of( - new RelWithProps(target1, "vr1"), - new RelWithProps(target2, "vr2")) - ); + owner.setRelatedNodes(List.of(new RelWithProps(target1, "vr1"), new RelWithProps(target2, "vr2"))); owner = repo3.save(owner); assertThat(owner.getId()).matches(validIdForCurrentNeo4j()); - assertThat(owner.getRelatedNodes()) - .hasSize(2) - .allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull()) - .extracting(RelWithProps::getRelValue) - .containsExactlyInAnyOrder("vr1", "vr2"); + assertThat(owner.getRelatedNodes()).hasSize(2) + .allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull()) + .extracting(RelWithProps::getRelValue) + .containsExactlyInAnyOrder("vr1", "vr2"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1) - WHERE elementId(n) = $id1 - AND r.relValue IN $rv - RETURN count(*)"""), - Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))).single().get(0).asLong(); + MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1) + WHERE elementId(n) = $id1 + AND r.relValue IN $rv + RETURN count(*)"""), Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(2L); } } @Test - void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - ownerId = session.run(adaptQueryTo44IfNecessary(""" - CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'}) - RETURN elementId(n)""") - ).single().get(0).asString(); + ownerId = session + .run(adaptQueryTo44IfNecessary( + """ + CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'}) + RETURN elementId(n)""")) + .single() + .get(0) + .asString(); } var owner = repo3.findById(ownerId).orElseThrow(); @@ -278,18 +283,25 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) - WHERE elementId(n) = $id1 - AND r.relValue IN $rv - RETURN count(*)"""), - Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", List.of("whatever"))).single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) + WHERE elementId(n) = $id1 + AND r.relValue IN $rv + RETURN count(*)"""), + Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", + List.of("whatever"))) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test - void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId4("owner"); var intermediate = new NodeWithGeneratedId4.Intermediate(); @@ -311,22 +323,30 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { AND elementId(i) = $id2 AND elementId(e) = $id3 RETURN count(*)"""; - var count = session.run(adaptQueryTo44IfNecessary(query), - Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) - .single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary(query), Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", + owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test - void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - ownerId = session.run(adaptQueryTo44IfNecessary(""" - CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'}) - RETURN elementId(n)""")) - .single().get(0).asString(); + ownerId = session + .run(adaptQueryTo44IfNecessary( + """ + CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'}) + RETURN elementId(n)""")) + .single() + .get(0) + .asString(); } var owner = repo4.findAllById(List.of(ownerId)).get(0); @@ -342,52 +362,75 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2}) - WHERE elementId(n) = $id1 - AND elementId(i) = $id2 - AND elementId(e) = $id3 - RETURN count(*)"""), - Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) - .single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2}) + WHERE elementId(n) = $id1 + AND elementId(i) = $id2 + AND elementId(e) = $id3 + RETURN count(*)"""), + Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), + "id3", owner.getIntermediate().getEnd().getId())) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test @Tag("GH-2927") - void fluentOpsMustUseCypherDSLConfig( - LogbackCapture logbackCapture, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture, - @Autowired Neo4jTemplate neo4jTemplate) { + void fluentOpsMustUseCypherDSLConfig(LogbackCapture logbackCapture, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Neo4jTemplate neo4jTemplate) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - session.run("MERGE (n:" + Thing.THING_LABEL + "{foo: 'bar'})").consume(); + session.run("MERGE (n:" + Thing.THING_LABEL + "{foo: 'bar'})").consume(); } var thingNode = Cypher.node(Thing.THING_LABEL); var cypherStatement = Statement.builder() - .match(thingNode) - .where(Cypher.elementId(thingNode).eq(Cypher.literalOf("test"))) - .returning(thingNode) - .build(); + .match(thingNode) + .where(Cypher.elementId(thingNode).eq(Cypher.literalOf("test"))) + .returning(thingNode) + .build(); neo4jTemplate.find(Thing.class).matching(cypherStatement).one(); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } + interface Repo1 extends Neo4jRepository { + + NodeWithGeneratedId1 findByIdIn(List ids); + + } + + interface Repo2 extends Neo4jRepository { + + NodeWithGeneratedId2 findByRelatedNodesIdIn(List ids); + + } + + interface Repo3 extends Neo4jRepository { + + } + + interface Repo4 extends Neo4jRepository { + + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends Neo4jImperativeTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new Neo4jTransactionManager(driver, databaseNameProvider, @@ -395,6 +438,7 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -404,5 +448,7 @@ public class ImperativeElementIdIT extends AbstractElementIdTestBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId1.java index c41b43aa4..72d01d548 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId1.java @@ -35,19 +35,20 @@ public class NodeWithGeneratedId1 { this.value = value; } + public String getId() { + return this.id; + } + public void setId(String id) { this.id = id; } - public String getId() { - return id; - } - public String getValue() { - return value; + return this.value; } public void setValue(String value) { this.value = value; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId2.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId2.java index 96f5cb131..8e8db3bca 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId2.java @@ -42,22 +42,23 @@ public class NodeWithGeneratedId2 { } public String getId() { - return id; + return this.id; } public String getValue() { - return value; + return this.value; + } + + public void setValue(String value) { + this.value = value; } public List getRelatedNodes() { - return relatedNodes; + return this.relatedNodes; } public void setRelatedNodes(List relatedNodes) { this.relatedNodes = relatedNodes; } - public void setValue(String value) { - this.value = value; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId3.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId3.java index 1ea361744..8b6e6f13e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId3.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId3.java @@ -42,22 +42,23 @@ public class NodeWithGeneratedId3 { } public String getId() { - return id; + return this.id; } public String getValue() { - return value; + return this.value; + } + + public void setValue(String value) { + this.value = value; } public List getRelatedNodes() { - return relatedNodes; + return this.relatedNodes; } public void setRelatedNodes(List relatedNodes) { this.relatedNodes = relatedNodes; } - public void setValue(String value) { - this.value = value; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId4.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId4.java index 58e67b9a2..7c5e5fc33 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId4.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/NodeWithGeneratedId4.java @@ -26,33 +26,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class NodeWithGeneratedId4 { - - @Node - static class Intermediate { - @Id - @GeneratedValue - private String id; - - @Relationship - NodeWithGeneratedId4 end; - - String getId() { - return id; - } - - void setId(String id) { - this.id = id; - } - - NodeWithGeneratedId4 getEnd() { - return end; - } - - void setEnd(NodeWithGeneratedId4 end) { - this.end = end; - } - } - @Id @GeneratedValue private String id; @@ -67,22 +40,51 @@ public class NodeWithGeneratedId4 { } public String getId() { - return id; + return this.id; } public String getValue() { - return value; + return this.value; + } + + public void setValue(String value) { + this.value = value; } public Intermediate getIntermediate() { - return intermediate; + return this.intermediate; } public void setIntermediate(Intermediate intermediate) { this.intermediate = intermediate; } - public void setValue(String value) { - this.value = value; + @Node + static class Intermediate { + + @Relationship + NodeWithGeneratedId4 end; + + @Id + @GeneratedValue + private String id; + + String getId() { + return this.id; + } + + void setId(String id) { + this.id = id; + } + + NodeWithGeneratedId4 getEnd() { + return this.end; + } + + void setEnd(NodeWithGeneratedId4 end) { + this.end = end; + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java index d4e225a0c..4a31858e3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/ReactiveElementIdIT.java @@ -25,6 +25,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.driver.Driver; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,16 +44,14 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; /** - * Assertions that no {@code id()} calls are generated when no deprecated id types are present. - * This test deliberately uses blocking calls into reactor because it's really not about the reactive flows but about - * catching all the code paths that might interact with ids on the reactive side of things. Yes, Reactors testing tools - * are known. + * Assertions that no {@code id()} calls are generated when no deprecated id types are + * present. This test deliberately uses blocking calls into reactor because it's really + * not about the reactive flows but about catching all the code paths that might interact + * with ids on the reactive side of things. Yes, Reactors testing tools are known. * * @author Michael J. Simons */ @@ -58,21 +59,6 @@ import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(LogbackCapturingExtension.class) public class ReactiveElementIdIT extends AbstractElementIdTestBase { - - interface Repo1 extends ReactiveNeo4jRepository { - Mono findByIdIn(List ids); - } - - interface Repo2 extends ReactiveNeo4jRepository { - Mono findByRelatedNodesIdIn(List ids); - } - - interface Repo3 extends ReactiveNeo4jRepository { - } - - interface Repo4 extends ReactiveNeo4jRepository { - } - @Test void dontCallIdForDerivedQueriesWithInClause(LogbackCapture logbackCapture, @Autowired Repo1 repo1) { @@ -103,8 +89,7 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { var node = repo1.save(new NodeWithGeneratedId1("from-sdn-repo")).block(); assertThat(node).isNotNull(); - assertThat(node.getId()) - .matches(validIdForCurrentNeo4j()); + assertThat(node.getId()).matches(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @@ -112,75 +97,86 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { void simpleNodeAllCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) { var nodes = repo1.saveAll(List.of(new NodeWithGeneratedId1("from-sdn-repo"))).collectList().block(); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String id; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId(); + id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode() + .elementId(); } var optionalNode = Optional.ofNullable(repo1.findById(id).block()); - assertThat(optionalNode).map(NodeWithGeneratedId1::getValue) - .hasValue("whatever"); + assertThat(optionalNode).map(NodeWithGeneratedId1::getValue).hasValue("whatever"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId(); + session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode() + .elementId(); } var nodes = repo1.findAll().collectList().block(); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { NodeWithGeneratedId1 node; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode(); + var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode(); node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited"); node.setId(dbNode.elementId()); } node = repo1.save(node).block(); - assertThat(node).extracting(NodeWithGeneratedId1::getValue) - .isEqualTo("whatever_edited"); + assertThat(node).extracting(NodeWithGeneratedId1::getValue).isEqualTo("whatever_edited"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { NodeWithGeneratedId1 node; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode(); + var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n") + .single() + .get("n") + .asNode(); node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited"); node.setId(dbNode.elementId()); } var nodes = repo1.saveAll(List.of(node)).collectList().block(); - assertThat(nodes).isNotEmpty() - .extracting(NodeWithGeneratedId1::getId) - .allMatch(validIdForCurrentNeo4j()); + assertThat(nodes).isNotEmpty().extracting(NodeWithGeneratedId1::getId).allMatch(validIdForCurrentNeo4j()); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, + @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId2("owner"); owner.setRelatedNodes(List.of(new NodeWithGeneratedId1("child1"), new NodeWithGeneratedId1("child2"))); @@ -189,29 +185,30 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { assertThat(owner).isNotNull(); assertThat(owner.getId()).isNotNull(); assertThat(owner.getRelatedNodes()) - .allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j())); + .allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j())); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } @Test - void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, + @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; String ownedId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var row = session.run("CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *").single(); + var row = session.run( + "CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *") + .single(); ownerId = row.get("n").asNode().elementId(); ownedId = row.get("m").asNode().elementId(); } var owner = repo2.findById(ownerId).block(); assertThat(owner).isNotNull(); - assertThat(owner.getRelatedNodes()) - .hasSize(1) - .first() - .extracting(NodeWithGeneratedId1::getId) - .isEqualTo(ownedId); - + assertThat(owner.getRelatedNodes()).hasSize(1) + .first() + .extracting(NodeWithGeneratedId1::getId) + .isEqualTo(ownedId); owner.getRelatedNodes().get(0).setValue("owned_changed"); owner.setValue("owner_changed"); @@ -221,59 +218,67 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) - WHERE elementId(n) = $id1 AND elementId(m) = $id2 - RETURN count(*)"""), - Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)).single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) + WHERE elementId(n) = $id1 AND elementId(m) = $id2 + RETURN count(*)"""), + Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)) + .single() + .get(0) + .asLong(); assertThat(count).isOne(); } } @Test - void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId3("owner"); var target1 = new NodeWithGeneratedId1("target1"); var target2 = new NodeWithGeneratedId1("target2"); - owner.setRelatedNodes( - List.of( - new RelWithProps(target1, "vr1"), - new RelWithProps(target2, "vr2")) - ); + owner.setRelatedNodes(List.of(new RelWithProps(target1, "vr1"), new RelWithProps(target2, "vr2"))); owner = repo3.save(owner).block(); assertThat(owner).isNotNull(); assertThat(owner.getId()).matches(validIdForCurrentNeo4j()); - assertThat(owner.getRelatedNodes()) - .hasSize(2) - .allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull()) - .extracting(RelWithProps::getRelValue) - .containsExactlyInAnyOrder("vr1", "vr2"); + assertThat(owner.getRelatedNodes()).hasSize(2) + .allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull()) + .extracting(RelWithProps::getRelValue) + .containsExactlyInAnyOrder("vr1", "vr2"); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1) - WHERE elementId(n) = $id1 - AND r.relValue IN $rv - RETURN count(*)"""), - Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))).single().get(0).asLong(); + MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1) + WHERE elementId(n) = $id1 + AND r.relValue IN $rv + RETURN count(*)"""), Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(2L); } } @Test - void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - ownerId = session.run(adaptQueryTo44IfNecessary(""" - CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'}) - RETURN elementId(n)""") - ).single().get(0).asString(); + ownerId = session + .run(adaptQueryTo44IfNecessary( + """ + CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'}) + RETURN elementId(n)""")) + .single() + .get(0) + .asString(); } var owner = repo3.findById(ownerId).block(); @@ -288,18 +293,25 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) - WHERE elementId(n) = $id1 - AND r.relValue IN $rv - RETURN count(*)"""), - Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", List.of("whatever"))).single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2}) + WHERE elementId(n) = $id1 + AND r.relValue IN $rv + RETURN count(*)"""), + Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", + List.of("whatever"))) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test - void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { var owner = new NodeWithGeneratedId4("owner"); var intermediate = new NodeWithGeneratedId4.Intermediate(); @@ -322,22 +334,30 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { AND elementId(i) = $id2 AND elementId(e) = $id3 RETURN count(*)"""; - var count = session.run(adaptQueryTo44IfNecessary(query), - Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) - .single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary(query), Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", + owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test - void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { + void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, + @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { String ownerId; try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - ownerId = session.run(adaptQueryTo44IfNecessary(""" - CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'}) - RETURN elementId(n)""")) - .single().get(0).asString(); + ownerId = session + .run(adaptQueryTo44IfNecessary( + """ + CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'}) + RETURN elementId(n)""")) + .single() + .get(0) + .asString(); } var owner = repo4.findAllById(List.of(ownerId)).blockFirst(); @@ -355,25 +375,27 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j); try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - var count = session.run(adaptQueryTo44IfNecessary(""" - MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2}) - WHERE elementId(n) = $id1 - AND elementId(i) = $id2 - AND elementId(e) = $id3 - RETURN count(*)"""), - Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId())) - .single().get(0).asLong(); + var count = session + .run(adaptQueryTo44IfNecessary( + """ + MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2}) + WHERE elementId(n) = $id1 + AND elementId(i) = $id2 + AND elementId(e) = $id3 + RETURN count(*)"""), + Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), + "id3", owner.getIntermediate().getEnd().getId())) + .single() + .get(0) + .asLong(); assertThat(count).isEqualTo(1L); } } @Test @Tag("GH-2927") - void fluentOpsMustUseCypherDSLConfig( - LogbackCapture logbackCapture, - @Autowired Driver driver, - @Autowired BookmarkCapture bookmarkCapture, - @Autowired ReactiveNeo4jTemplate neo4jTemplate) { + void fluentOpsMustUseCypherDSLConfig(LogbackCapture logbackCapture, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture, @Autowired ReactiveNeo4jTemplate neo4jTemplate) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { session.run("MERGE (n:" + Thing.THING_LABEL + "{foo: 'bar'})").consume(); @@ -381,27 +403,47 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { var thingNode = Cypher.node(Thing.THING_LABEL); var cypherStatement = Statement.builder() - .match(thingNode) - .where(Cypher.elementId(thingNode).eq(Cypher.literalOf("test"))) - .returning(thingNode) - .build(); - neo4jTemplate.find(Thing.class).matching(cypherStatement).all().as(StepVerifier::create) - .verifyComplete(); + .match(thingNode) + .where(Cypher.elementId(thingNode).eq(Cypher.literalOf("test"))) + .returning(thingNode) + .build(); + neo4jTemplate.find(Thing.class).matching(cypherStatement).all().as(StepVerifier::create).verifyComplete(); assertThatLogMessageDoNotIndicateIDUsage(logbackCapture); } + interface Repo1 extends ReactiveNeo4jRepository { + + Mono findByIdIn(List ids); + + } + + interface Repo2 extends ReactiveNeo4jRepository { + + Mono findByRelatedNodesIdIn(List ids); + + } + + interface Repo3 extends ReactiveNeo4jRepository { + + } + + interface Repo4 extends ReactiveNeo4jRepository { + + } + @Configuration @EnableTransactionManagement @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) static class Config extends Neo4jReactiveTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, @@ -409,6 +451,7 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -418,5 +461,7 @@ public class ReactiveElementIdIT extends AbstractElementIdTestBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/RelWithProps.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/RelWithProps.java index ea1308a4d..3e97ccca9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/RelWithProps.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/RelWithProps.java @@ -39,7 +39,7 @@ public class RelWithProps { } public String getId() { - return id; + return this.id; } public void setId(String id) { @@ -47,7 +47,7 @@ public class RelWithProps { } public String getRelValue() { - return relValue; + return this.relValue; } public void setRelValue(String relValue) { @@ -55,10 +55,11 @@ public class RelWithProps { } public NodeWithGeneratedId1 getTarget() { - return target; + return this.target; } public void setTarget(NodeWithGeneratedId1 target) { this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/Thing.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/Thing.java index 35a01196b..483365c84 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/Thing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/Thing.java @@ -30,5 +30,7 @@ public class Thing { @Id @GeneratedValue String id; + String name; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/A.java b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/A.java index ed1554daa..103fc7eea 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/A.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/A.java @@ -40,11 +40,11 @@ public class A { private B b; public UUID getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -52,10 +52,11 @@ public class A { } public B getB() { - return b; + return this.b; } public void setB(B b) { this.b = b; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/ARepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/ARepository.java index 92d3eeef1..226b4b68e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/ARepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/ARepository.java @@ -25,4 +25,5 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * @author Michael Simons */ public interface ARepository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/B.java b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/B.java index 1c04d550d..061250477 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/B.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/qbe/B.java @@ -28,6 +28,7 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class B { + @Id @GeneratedValue(GeneratedValue.UUIDGenerator.class) UUID id; @@ -39,14 +40,15 @@ public class B { } public UUID getId() { - return id; + return this.id; } public String getAnotherName() { - return anotherName; + return this.anotherName; } public void setAnotherName(String anotherName) { this.anotherName = anotherName; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/kotlin/KotlinIT.java b/src/test/java/org/springframework/data/neo4j/integration/kotlin/KotlinIT.java index c7ddd7b66..0d5174534 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/kotlin/KotlinIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/kotlin/KotlinIT.java @@ -15,18 +15,16 @@ */ package org.springframework.data.neo4j.integration.kotlin; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -39,11 +37,14 @@ import org.springframework.data.neo4j.integration.shared.common.TestPersonReposi import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier * @author Michael J. Simons @@ -52,7 +53,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @Neo4jIntegrationTest class KotlinIT { - private final static String PERSON_NAME = "test"; + private static final String PERSON_NAME = "test"; private static Neo4jConnectionSupport neo4jConnectionSupport; @@ -61,19 +62,20 @@ class KotlinIT { @BeforeEach void setup(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n").consume(); - transaction.run("CREATE (n:KotlinPerson), " - + " (n)-[:WORKS_IN{since: 2019}]->(:KotlinClub{name: 'Golf club'}) SET n.name = $personName", - Values.parameters("personName", PERSON_NAME)) - .consume(); - transaction.run("CREATE (p1:TestPerson {id: \"first\", name: \"First name\"})\n" - + "CREATE (p2:TestPerson {id: \"second\"})\n" - + "CREATE (d:TestDepartment {id: \"department\", name: \"Test\"})\n" - + "CREATE (p1)-[:MEMBER_OF]->(d)\n" - + "CREATE (p2)-[:MEMBER_OF]->(d)\n").consume(); + transaction + .run("CREATE (n:KotlinPerson), " + + " (n)-[:WORKS_IN{since: 2019}]->(:KotlinClub{name: 'Golf club'}) SET n.name = $personName", + Values.parameters("personName", PERSON_NAME)) + .consume(); + transaction + .run("CREATE (p1:TestPerson {id: \"first\", name: \"First name\"})\n" + + "CREATE (p2:TestPerson {id: \"second\"})\n" + + "CREATE (d:TestDepartment {id: \"department\", name: \"Test\"})\n" + + "CREATE (p1)-[:MEMBER_OF]->(d)\n" + "CREATE (p2)-[:MEMBER_OF]->(d)\n") + .consume(); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -87,23 +89,24 @@ class KotlinIT { KotlinPerson person = people.iterator().next(); assertThat(person.getClubs()).extracting(KotlinClubRelationship::getSince).containsExactly(2019); assertThat(person.getClubs()).extracting(KotlinClubRelationship::getClub) - .extracting(KotlinClub::getName).containsExactly("Golf club"); + .extracting(KotlinClub::getName) + .containsExactly("Golf club"); } @Test // GH-2272 void primitiveDefaultValuesShouldWork(@Autowired TestPersonRepository repository) { Iterable people = repository.findAll(); - assertThat(people) - .allSatisfy(p -> { - assertThat(p.getOtherPrimitive()).isEqualTo(32); - assertThat(p.getSomeTruth()).isTrue(); - if (p.getId().equalsIgnoreCase("first")) { - assertThat(p.getName()).isEqualTo("First name"); - } else { - assertThat(p.getName()).isEqualTo("Unknown"); - } - }); + assertThat(people).allSatisfy(p -> { + assertThat(p.getOtherPrimitive()).isEqualTo(32); + assertThat(p.getSomeTruth()).isTrue(); + if (p.getId().equalsIgnoreCase("first")) { + assertThat(p.getName()).isEqualTo("First name"); + } + else { + assertThat(p.getName()).isEqualTo("Unknown"); + } + }); } @Configuration @@ -112,25 +115,30 @@ class KotlinIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/A.java b/src/test/java/org/springframework/data/neo4j/integration/lite/A.java index 82cd14185..86cfd02f7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/A.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/A.java @@ -21,12 +21,13 @@ package org.springframework.data.neo4j.integration.lite; * @author Michael J. Simons */ public class A { + private String outer; private B nested; public String getOuter() { - return outer; + return this.outer; } public void setOuter(String outer) { @@ -34,10 +35,11 @@ public class A { } public B getNested() { - return nested; + return this.nested; } public void setNested(B nested) { this.nested = nested; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/B.java b/src/test/java/org/springframework/data/neo4j/integration/lite/B.java index 3fdfb2541..409b913e0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/B.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/B.java @@ -21,13 +21,15 @@ package org.springframework.data.neo4j.integration.lite; * @author Michael J. Simons */ public class B { + private String inner; public String getInner() { - return inner; + return this.inner; } public void setInner(String inner) { this.inner = inner; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/LightweightMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/lite/LightweightMappingIT.java index d4ff76df3..2eb8c2277 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/LightweightMappingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/LightweightMappingIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.lite; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Optional; @@ -24,6 +22,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,6 +37,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + @Neo4jIntegrationTest class LightweightMappingIT { @@ -49,15 +50,13 @@ class LightweightMappingIT { try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); // language=cypher - session.run( - """ - CREATE (u1:User {login: 'michael', id: randomUUID()}) - CREATE (u2:User {login: 'gerrit', id: randomUUID()}) - CREATE (so1:SomeDomainObject {name: 'name1', id: randomUUID()}) - CREATE (so2:SomeDomainObject {name: 'name2', id: randomUUID()}) - CREATE (so1)<-[:OWNS]-(u1)-[:OWNS]->(so2) - """ - ); + session.run(""" + CREATE (u1:User {login: 'michael', id: randomUUID()}) + CREATE (u2:User {login: 'gerrit', id: randomUUID()}) + CREATE (so1:SomeDomainObject {name: 'name1', id: randomUUID()}) + CREATE (so2:SomeDomainObject {name: 'name2', id: randomUUID()}) + CREATE (so1)<-[:OWNS]-(u1)-[:OWNS]->(so2) + """); bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -66,11 +65,10 @@ class LightweightMappingIT { void getAllFlatShouldWork(@Autowired SomeDomainRepository repository) { Collection dtos = repository.getAllFlat(); - assertThat(dtos).hasSize(10) - .allSatisfy(dto -> { - assertThat(dto.counter).isGreaterThan(0); - assertThat(dto.resyncId).isNotNull(); - }); + assertThat(dtos).hasSize(10).allSatisfy(dto -> { + assertThat(dto.counter).isGreaterThan(0); + assertThat(dto.resyncId).isNotNull(); + }); } @Test @@ -87,30 +85,22 @@ class LightweightMappingIT { void getAllNestedShouldWork(@Autowired SomeDomainRepository repository) { Collection dtos = repository.getNestedStuff(); - assertThat(dtos).hasSize(1) - .first() - .satisfies(dto -> { - assertThat(dto.counter).isEqualTo(4711L); - assertThat(dto.resyncId).isNotNull(); - assertThat(dto.user) - .isNotNull() - .extracting(User::getLogin) - .isEqualTo("michael"); - assertThat(dto.user.getOwnedObjects()) - .hasSize(2); + assertThat(dtos).hasSize(1).first().satisfies(dto -> { + assertThat(dto.counter).isEqualTo(4711L); + assertThat(dto.resyncId).isNotNull(); + assertThat(dto.user).isNotNull().extracting(User::getLogin).isEqualTo("michael"); + assertThat(dto.user.getOwnedObjects()).hasSize(2); - }); + }); } - @Test void getTestedDTOsShouldWork(@Autowired SomeDomainRepository repository) { Optional dto = repository.getOneNestedDTO(); assertThat(dto).hasValueSatisfying(v -> { assertThat(v.getOuter()).isEqualTo("av"); - assertThat(v.getNested()).isNotNull() - .extracting(B::getInner).isEqualTo("bv"); + assertThat(v.getNested()).isNotNull().extracting(B::getInner).isEqualTo("bv"); }); } @@ -121,26 +111,30 @@ class LightweightMappingIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/MyDTO.java b/src/test/java/org/springframework/data/neo4j/integration/lite/MyDTO.java index 2be005265..948a92dfd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/MyDTO.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/MyDTO.java @@ -21,9 +21,11 @@ package org.springframework.data.neo4j.integration.lite; * @author Michael J. Simons */ public class MyDTO { + String resyncId; Long counter; User user; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainObject.java b/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainObject.java index 6e369462e..527abf8fc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainObject.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainObject.java @@ -29,21 +29,22 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class SomeDomainObject { + private final String name; + @Id @GeneratedValue private UUID id; - private final String name; - public SomeDomainObject(String name) { this.name = name; } public UUID getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainRepository.java b/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainRepository.java index 6d0201534..d332a1e00 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/SomeDomainRepository.java @@ -60,4 +60,5 @@ public interface SomeDomainRepository extends Neo4jRepository getOneNestedDTO(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/lite/User.java b/src/test/java/org/springframework/data/neo4j/integration/lite/User.java index a02532444..aa071dd9c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/lite/User.java +++ b/src/test/java/org/springframework/data/neo4j/integration/lite/User.java @@ -31,12 +31,12 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class User { + private final String login; + @Id @GeneratedValue private UUID id; - private final String login; - @Relationship(direction = Relationship.Direction.OUTGOING, type = "OWNS") private List ownedObjects; @@ -45,22 +45,23 @@ public class User { } public UUID getId() { - return id; - } - - public String getLogin() { - return login; + return this.id; } public void setId(UUID id) { this.id = id; } + public String getLogin() { + return this.login; + } + public List getOwnedObjects() { - return ownedObjects; + return this.ownedObjects; } public void setOwnedObjects(List ownedObjects) { this.ownedObjects = ownedObjects; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/misc/ConcreteImplementationTwo.java b/src/test/java/org/springframework/data/neo4j/integration/misc/ConcreteImplementationTwo.java index bd449861f..1fc9bf5d7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/misc/ConcreteImplementationTwo.java +++ b/src/test/java/org/springframework/data/neo4j/integration/misc/ConcreteImplementationTwo.java @@ -19,10 +19,11 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.integration.issues.gh2530.InitialEntities; /** - * Moved far away from the issue integration tests so that it actually fulfills its purpose on being the entity to get - * discovered later. + * Moved far away from the issue integration tests so that it actually fulfills its + * purpose on being the entity to get discovered later. */ @Node public class ConcreteImplementationTwo extends InitialEntities.SomethingInBetween implements InitialEntities.SpecialKind { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/misc/IdLoggingIT.java b/src/test/java/org/springframework/data/neo4j/integration/misc/IdLoggingIT.java index 96e09c130..8553e4de4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/misc/IdLoggingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/misc/IdLoggingIT.java @@ -15,15 +15,15 @@ */ package org.springframework.data.neo4j.integration.misc; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - import java.util.function.Predicate; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -38,8 +38,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.ServerVersion; import org.springframework.transaction.annotation.EnableTransactionManagement; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; @Neo4jIntegrationTest @ExtendWith(LogbackCapturingExtension.class) @@ -47,6 +47,73 @@ class IdLoggingIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + static boolean isGreaterThanOrEqualNeo4j5() { + return neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v5_0_0); + } + + @EnabledIf("isGreaterThanOrEqualNeo4j5") + @Test + void idWarningShouldBeSuppressed(LogbackCapture logbackCapture, @Autowired Neo4jClient neo4jClient) { + + // Was not able to combine the autowiring of capture and the client here + for (Boolean enabled : new Boolean[] { true, false, null }) { + + Logger logger = (Logger) org.slf4j.LoggerFactory + .getLogger("org.springframework.data.neo4j.cypher.deprecation"); + Level originalLevel = logger.getLevel(); + logger.setLevel(Level.DEBUG); + + Boolean oldValue = null; + if (enabled != null) { + oldValue = Neo4jClient.SUPPRESS_ID_DEPRECATIONS.getAndSet(enabled); + } + + try { + assertThatCode(() -> neo4jClient.query("CREATE (n:XXXIdTest) RETURN id(n)").fetch().all()) + .doesNotThrowAnyException(); + Predicate stringPredicate = msg -> msg + .contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning"); + + if (enabled == null || enabled) { + assertThat(logbackCapture.getFormattedMessages()).noneMatch(stringPredicate); + } + else { + assertThat(logbackCapture.getFormattedMessages()).anyMatch(stringPredicate); + } + } + finally { + logbackCapture.clear(); + logger.setLevel(originalLevel); + if (oldValue != null) { + Neo4jClient.SUPPRESS_ID_DEPRECATIONS.set(oldValue); + } + } + } + } + + @EnabledIf("isGreaterThanOrEqualNeo4j5") + @Test + void otherDeprecationsWarningsShouldNotBeSuppressed(LogbackCapture logbackCapture, + @Autowired Neo4jClient neo4jClient) { + + Logger logger = (Logger) org.slf4j.LoggerFactory.getLogger("org.springframework.data.neo4j.cypher.deprecation"); + Level originalLevel = logger.getLevel(); + logger.setLevel(Level.DEBUG); + + try { + assertThatCode( + () -> neo4jClient.query("MATCH (n) CALL {WITH n RETURN count(n) AS cnt} RETURN *").fetch().all()) + .doesNotThrowAnyException(); + assertThat(logbackCapture.getFormattedMessages()) + .anyMatch(msg -> msg.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning")) + .anyMatch(msg -> msg + .contains("CALL subquery without a variable scope clause is now deprecated. Use CALL (n) { ... }")); + } + finally { + logger.setLevel(originalLevel); + } + } + @Configuration @EnableTransactionManagement @ComponentScan @@ -58,6 +125,7 @@ class IdLoggingIT { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -66,65 +134,7 @@ class IdLoggingIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } - static boolean isGreaterThanOrEqualNeo4j5() { - return neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v5_0_0); - } - - @EnabledIf("isGreaterThanOrEqualNeo4j5") - @Test - void idWarningShouldBeSuppressed(LogbackCapture logbackCapture, @Autowired Neo4jClient neo4jClient) { - - // Was not able to combine the autowiring of capture and the client here - for (Boolean enabled : new Boolean[] {true, false, null}) { - - Logger logger = (Logger) org.slf4j.LoggerFactory.getLogger("org.springframework.data.neo4j.cypher.deprecation"); - Level originalLevel = logger.getLevel(); - logger.setLevel(Level.DEBUG); - - Boolean oldValue = null; - if (enabled != null) { - oldValue = Neo4jClient.SUPPRESS_ID_DEPRECATIONS.getAndSet(enabled); - } - - try { - assertThatCode(() -> neo4jClient.query( - "CREATE (n:XXXIdTest) RETURN id(n)").fetch().all()).doesNotThrowAnyException(); - Predicate stringPredicate = msg -> msg.contains( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning"); - - if (enabled == null || enabled) { - assertThat(logbackCapture.getFormattedMessages()).noneMatch(stringPredicate); - } else { - assertThat(logbackCapture.getFormattedMessages()).anyMatch(stringPredicate); - } - } finally { - logbackCapture.clear(); - logger.setLevel(originalLevel); - if (oldValue != null) { - Neo4jClient.SUPPRESS_ID_DEPRECATIONS.set(oldValue); - } - } - } - } - - @EnabledIf("isGreaterThanOrEqualNeo4j5") - @Test - void otherDeprecationsWarningsShouldNotBeSuppressed(LogbackCapture logbackCapture, @Autowired Neo4jClient neo4jClient) { - - Logger logger = (Logger) org.slf4j.LoggerFactory.getLogger("org.springframework.data.neo4j.cypher.deprecation"); - Level originalLevel = logger.getLevel(); - logger.setLevel(Level.DEBUG); - - try { - assertThatCode(() -> neo4jClient.query( - "MATCH (n) CALL {WITH n RETURN count(n) AS cnt} RETURN *").fetch().all()).doesNotThrowAnyException(); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(msg -> msg.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning")) - .anyMatch(msg -> msg.contains("CALL subquery without a variable scope clause is now deprecated. Use CALL (n) { ... }")); - } finally { - logger.setLevel(originalLevel); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/imperative/AdvancedMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/movies/imperative/AdvancedMappingIT.java index 4f5e3e9f7..1125cd04f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/imperative/AdvancedMappingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/imperative/AdvancedMappingIT.java @@ -15,14 +15,25 @@ */ package org.springframework.data.neo4j.integration.movies.imperative; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -38,27 +49,16 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.repository.query.Param; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons - * @soundtrack Body Count - Manslaughter */ @Neo4jIntegrationTest class AdvancedMappingIT { @@ -76,106 +76,13 @@ class AdvancedMappingIT { } } - interface MovieProjectionWithActorProjection { - String getTitle(); - - List getActors(); - - interface ActorProjection { - List getRoles(); - - PersonProjection getPerson(); - - interface PersonProjection { - - String getName(); - - List getActedIn(); - } - } - } - - interface MovieProjection { - - String getTitle(); - - List getActors(); - } - - static class MovieDTO { - - private final String title; - - private final List actors; - - MovieDTO(String title, List actors) { - this.title = title; - this.actors = actors; - } - - public String getTitle() { - return title; - } - - public List getActors() { - return actors; - } - } - - interface MovieWithSequelProjection { - String getTitle(); - MovieWithSequelProjection getSequel(); - } - - interface MovieWithSequelEntity { - String getTitle(); - Movie getSequel(); - } - - interface MovieWithMovieList { - String getTitle(); - - List getSequel(); - } - - interface MovieRepository extends Neo4jRepository { - - MovieProjection findProjectionByTitle(String title); - - MovieDTO findDTOByTitle(String title); - - MovieProjectionWithActorProjection findProjectionWithProjectionByTitle(@Param("title") String title); - - @Query("MATCH p=(movie:Movie)<-[r:ACTED_IN]-(n:Person) WHERE movie.title=$title RETURN collect(p)") - Movie customPathQueryMovieFind(@Param("title") String title); - - @Query("MATCH p=(movie:Movie)<-[r:ACTED_IN]-(n:Person) WHERE movie.title=$title RETURN collect(p)") - List customPathQueryMoviesFind(@Param("title") String title); - - MovieWithSequelProjection findProjectionByTitleAndDescription(String title, String description); - - MovieWithSequelEntity findByTitleAndDescription(String title, String description); - - @Query("MATCH (m:Movie{title:'The Matrix'})<-[a:ACTED_IN]-(p:Person) WITH a,p,m order by p.name return m, collect(a), collect(p)") - Movie findMatrixWithSortedAscActors(); - - @Query("MATCH (m:Movie{title:'The Matrix'})<-[a:ACTED_IN]-(p:Person) WITH a,p,m order by p.name DESC return m, collect(a), collect(p)") - Movie findMatrixWithSortedDescActors(); - - @Query("MATCH p=(:Movie{title:'The Matrix'}) return p") - Movie findSingleNodeWithPath(); - - @Query("MATCH p=(m:Movie{title:'The Matrix'})<-[:ACTED_IN]-(:Person) return m, collect(nodes(p)), collect(relationships(p))") - List findMultipleSameTypeRelationshipsWithPath(); - } - @Test // GH-1906 void nestedSelfRelationshipsFromCustomQueryShouldWork(@Autowired Neo4jTemplate template) { Optional optionalPartner = template.findOne( "MATCH p=(partner:Partner {code: $partnerCode})-[:CHILD_ORGANISATIONS*0..4]->(org:Organisation) \n" - + "UNWIND nodes(p) as node UNWIND relationships(p) as rel\n" - + "RETURN partner, collect(distinct node), collect(distinct rel)", + + "UNWIND nodes(p) as node UNWIND relationships(p) as rel\n" + + "RETURN partner, collect(distinct node), collect(distinct rel)", Collections.singletonMap("partnerCode", "partner-one"), Partner.class); assertThat(optionalPartner).hasValueSatisfying(p -> { @@ -185,8 +92,9 @@ class AdvancedMappingIT { Organisation org1 = p.getOrganisations().get(0); assertThat(org1.getCode()).isEqualTo("org-1"); - Map org1Childs = org1.getOrganisations().stream() - .collect(Collectors.toMap(Organisation::getCode, Function.identity())); + Map org1Childs = org1.getOrganisations() + .stream() + .collect(Collectors.toMap(Organisation::getCode, Function.identity())); assertThat(org1Childs).hasSize(2); assertThat(org1Childs).hasEntrySatisfying("org-2", o -> assertThat(o.getOrganisations()).hasSize(1)); @@ -195,8 +103,9 @@ class AdvancedMappingIT { Organisation org3 = org1Childs.get("org-2").getOrganisations().get(0); assertThat(org3.getCode()).isEqualTo("org-3"); - Map org3Childs = org3.getOrganisations().stream() - .collect(Collectors.toMap(Organisation::getCode, Function.identity())); + Map org3Childs = org3.getOrganisations() + .stream() + .collect(Collectors.toMap(Organisation::getCode, Function.identity())); assertThat(org3Childs).containsKeys("org-4", "org-5"); }); } @@ -211,7 +120,8 @@ class AdvancedMappingIT { @Test void cyclicMappingShouldReturnResultForFindAllById(@Autowired MovieRepository repository) { - List movies = repository.findAllById(Arrays.asList("The Matrix", "The Matrix Revolutions", "The Matrix Reloaded")); + List movies = repository + .findAllById(Arrays.asList("The Matrix", "The Matrix Revolutions", "The Matrix Reloaded")); assertThat(movies).hasSize(3); } @@ -241,8 +151,8 @@ class AdvancedMappingIT { MovieDTO dtoProjection = movieRepository.findDTOByTitle("The Matrix"); assertThat(dtoProjection.getTitle()).isNotNull(); assertThat(dtoProjection.getActors()).extracting("name") - .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", - "Carrie-Anne Moss", "Hugo Weaving"); + .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", + "Carrie-Anne Moss", "Hugo Weaving"); } @Test // GH-2117 updated for GH-2320 @@ -253,60 +163,62 @@ class AdvancedMappingIT { // as the cyclic dependencies is pretty slow to retrieve from Neo4j // this does OOM in most setups. MovieProjectionWithActorProjection projection = movieRepository - .findProjectionWithProjectionByTitle("The Matrix"); + .findProjectionWithProjectionByTitle("The Matrix"); assertThat(projection.getTitle()).isNotNull(); - assertThat(projection.getActors()).extracting("person").extracting("name") - .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", - "Carrie-Anne Moss", "Hugo Weaving"); + assertThat(projection.getActors()).extracting("person") + .extracting("name") + .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", + "Carrie-Anne Moss", "Hugo Weaving"); assertThat(projection.getActors()).flatExtracting("roles") - .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); + .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); // second level mapping of entity cycle assertThat(projection.getActors()).extracting("person") - .allMatch(person -> - !((MovieProjectionWithActorProjection.ActorProjection.PersonProjection) person).getActedIn().isEmpty()); + .allMatch(person -> !((MovieProjectionWithActorProjection.ActorProjection.PersonProjection) person) + .getActedIn() + .isEmpty()); // n+1 level mapping of entity cycle - assertThat(projection.getActors()).extracting("person").flatExtracting("actedIn").extracting("directors") - .allMatch(directors -> !((Collection) directors).isEmpty()); + assertThat(projection.getActors()).extracting("person") + .flatExtracting("actedIn") + .extracting("directors") + .allMatch(directors -> !((Collection) directors).isEmpty()); } @Test // GH-2114 void bothStartAndEndNodeOfPathsMustBeLookedAt(@Autowired Neo4jTemplate template) { - // @ParameterizedTest does not work together with the parameter resolver for @Autowired - for (String query : new String[] { - "MATCH p=()-[:IS_SIBLING_OF]-> () RETURN p", - "MATCH (s)-[:IS_SIBLING_OF]-> (e) RETURN [s,e]" - }) { + // @ParameterizedTest does not work together with the parameter resolver for + // @Autowired + for (String query : new String[] { "MATCH p=()-[:IS_SIBLING_OF]-> () RETURN p", + "MATCH (s)-[:IS_SIBLING_OF]-> (e) RETURN [s,e]" }) { List people = template.findAll(query, Collections.emptyMap(), Person.class); - assertThat(people) - .extracting(Person::getName) - .containsExactlyInAnyOrder("Lilly Wachowski", "Lana Wachowski"); + assertThat(people).extracting(Person::getName) + .containsExactlyInAnyOrder("Lilly Wachowski", "Lana Wachowski"); } } @Test // GH-2114 void directionAndTypeLessPathMappingShouldWork(@Autowired Neo4jTemplate template) { - List people = template - .findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), Person.class); + List people = template.findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), + Person.class); assertThat(people).hasSize(6); } @Test // GH-2114 void mappingOfAPathWithOddNumberOfElementsShouldWorkFromStartToEnd(@Autowired Neo4jTemplate template) { - Map movies = template - .findAll(""" - MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) - WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') - RETURN p""", - Collections.emptyMap(), Movie.class) - .stream().collect(Collectors.toMap(Movie::getTitle, Function.identity())); + Map movies = template.findAll(""" + MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) + WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') + RETURN p""", Collections.emptyMap(), Movie.class) + .stream() + .collect(Collectors.toMap(Movie::getTitle, Function.identity())); assertThat(movies).hasSize(3); - // This is the actual test for the original issue… When the end node of a segment is not taken into account, Emil is not an actor + // This is the actual test for the original issue… When the end node of a segment + // is not taken into account, Emil is not an actor assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getActors()).isNotEmpty()); @@ -318,17 +230,17 @@ class AdvancedMappingIT { @Test // GH-2114 void mappingOfAPathWithEventNumberOfElementsShouldWorkFromStartToEnd(@Autowired Neo4jTemplate template) { - Map movies = template - .findAll(""" - MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) - WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') - RETURN p - """, - Collections.emptyMap(), Movie.class) - .stream().collect(Collectors.toMap(Movie::getTitle, Function.identity())); + Map movies = template.findAll(""" + MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) + WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') + RETURN p + """, Collections.emptyMap(), Movie.class) + .stream() + .collect(Collectors.toMap(Movie::getTitle, Function.identity())); assertThat(movies).hasSize(3); - // This is the actual test for the original issue… When the end node of a segment is not taken into account, Emil is not an actor + // This is the actual test for the original issue… When the end node of a segment + // is not taken into account, Emil is not an actor assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getActors()).isEmpty()); @@ -338,9 +250,9 @@ class AdvancedMappingIT { } /** - * Here all paths are going into multiple records. Each path will be one record. The elements in the path will be - * seen as aggregated on the server side and each of the aggregates will also be aggregated. - * + * Here all paths are going into multiple records. Each path will be one record. The + * elements in the path will be seen as aggregated on the server side and each of the + * aggregates will also be aggregated. * @param template Used for querying */ @Test // DATAGRAPH-1437 @@ -349,8 +261,7 @@ class AdvancedMappingIT { Map parameters = new HashMap<>(); parameters.put("person1", "Kevin Bacon"); parameters.put("person2", "Angela Scope"); - String cypherQuery = - "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + String cypherQuery = "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + "RETURN allPaths"; List people = template.findAll(cypherQuery, parameters, Person.class); @@ -359,7 +270,6 @@ class AdvancedMappingIT { /** * Here all paths are going into one single record. - * * @param template Used for querying */ @Test // DATAGRAPH-1437 @@ -368,8 +278,7 @@ class AdvancedMappingIT { Map parameters = new HashMap<>(); parameters.put("person1", "Kevin Bacon"); parameters.put("person2", "Angela Scope"); - String cypherQuery = - "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + String cypherQuery = "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + "RETURN collect(allPaths)"; List people = template.findAll(cypherQuery, parameters, Person.class); @@ -377,8 +286,8 @@ class AdvancedMappingIT { } /** - * This tests checks whether all nodes that fit a certain class along a path are mapped correctly. - * + * This tests checks whether all nodes that fit a certain class along a path are + * mapped correctly. * @param template Used for querying */ @Test // DATAGRAPH-1437 @@ -388,26 +297,26 @@ class AdvancedMappingIT { parameters.put("person1", "Kevin Bacon"); parameters.put("person2", "Angela Scope"); parameters.put("requiredMovie", "The Da Vinci Code"); - String cypherQuery = - "MATCH p=shortestPath((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "WHERE size([n IN nodes(p) WHERE n.title = $requiredMovie]) > 0\n" - + "RETURN p"; + String cypherQuery = "MATCH p=shortestPath((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "WHERE size([n IN nodes(p) WHERE n.title = $requiredMovie]) > 0\n" + "RETURN p"; List people = template.findAll(cypherQuery, parameters, Person.class); - assertThat(people) - .hasSize(4) - .extracting(Person::getName) - .contains("Kevin Bacon", "Jessica Thompson", - "Angela Scope"); // Two paths lead there, one with Ron Howard, one with Tom Hanks. - assertThat(people).element(2).extracting(Person::getReviewed) - .satisfies( - movies -> assertThat(movies).extracting(Movie::getTitle).containsExactly("The Da Vinci Code")); + assertThat(people).hasSize(4) + .extracting(Person::getName) + .contains("Kevin Bacon", "Jessica Thompson", "Angela Scope"); // Two paths + // lead there, + // one with + // Ron Howard, + // one with + // Tom Hanks. + assertThat(people).element(2) + .extracting(Person::getReviewed) + .satisfies(movies -> assertThat(movies).extracting(Movie::getTitle).containsExactly("The Da Vinci Code")); } /** - * This tests checks whether all nodes that fit a certain class along a path are mapped correctly and if the - * additional joined information is applied as well. - * + * This tests checks whether all nodes that fit a certain class along a path are + * mapped correctly and if the additional joined information is applied as well. * @param template Used for querying */ @Test // DATAGRAPH-1437 @@ -416,26 +325,22 @@ class AdvancedMappingIT { parameters.put("person1", "Kevin Bacon"); parameters.put("person2", "Meg Ryan"); parameters.put("requiredMovie", "The Da Vinci Code"); - String cypherQuery = - "MATCH p=shortestPath(\n" + String cypherQuery = "MATCH p=shortestPath(\n" + "(p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "WITH p, [n in nodes(p) WHERE n:Movie] as mn\n" - + "UNWIND mn as m\n" - + "MATCH (m) <-[r:DIRECTED]- (d:Person)\n" - + "RETURN p, collect(r), collect(d)"; + + "WITH p, [n in nodes(p) WHERE n:Movie] as mn\n" + "UNWIND mn as m\n" + + "MATCH (m) <-[r:DIRECTED]- (d:Person)\n" + "RETURN p, collect(r), collect(d)"; List movies = template.findAll(cypherQuery, parameters, Movie.class); - assertThat(movies) - .hasSize(2) - .allSatisfy(m -> assertThat(m.getDirectors()).isNotEmpty()) - .first() - .satisfies(m -> assertThat(m.getDirectors()).extracting(Person::getName) - .containsAnyOf("Ron Howard", "Rob Reiner")); + assertThat(movies).hasSize(2) + .allSatisfy(m -> assertThat(m.getDirectors()).isNotEmpty()) + .first() + .satisfies(m -> assertThat(m.getDirectors()).extracting(Person::getName) + .containsAnyOf("Ron Howard", "Rob Reiner")); } /** - * This tests checks if the result of a custom path based query will get mapped correctly to an instance - * of the defined type instead of a collection. + * This tests checks if the result of a custom path based query will get mapped + * correctly to an instance of the defined type instead of a collection. */ @Test // DATAGRAPH-2107 void customPathMappingResultsInScalarResultIfDefined(@Autowired MovieRepository movieRepository) { @@ -446,8 +351,8 @@ class AdvancedMappingIT { } /** - * This tests checks if the result of a custom path based query will get mapped correctly to a collection - * of the defined type with all the fields hydrated. + * This tests checks if the result of a custom path based query will get mapped + * correctly to a collection of the defined type with all the fields hydrated. */ @Test // DATAGRAPH-2109 void customPathMappingCollectionResultsInHydratedEntities(@Autowired MovieRepository movieRepository) { @@ -482,19 +387,20 @@ class AdvancedMappingIT { @Test // GH-2458 void findPreservesOrderFromResultAscInRelationshipList(@Autowired MovieRepository repository) { - assertThat(repository.findMatrixWithSortedAscActors().getActors()).extracting("person").extracting("name") - .containsExactly("Carrie-Anne Moss", "Emil Eifrem", "Gloria Foster", "Hugo Weaving", "Keanu Reeves", - "Laurence Fishburne"); + assertThat(repository.findMatrixWithSortedAscActors().getActors()).extracting("person") + .extracting("name") + .containsExactly("Carrie-Anne Moss", "Emil Eifrem", "Gloria Foster", "Hugo Weaving", "Keanu Reeves", + "Laurence Fishburne"); } @Test // GH-2458 void findPreservesOrderFromResultDescInRelationshipList(@Autowired MovieRepository repository) { - assertThat(repository.findMatrixWithSortedDescActors().getActors()).extracting("person").extracting("name") - .containsExactly("Laurence Fishburne", "Keanu Reeves", "Hugo Weaving", "Gloria Foster", "Emil Eifrem", - "Carrie-Anne Moss"); + assertThat(repository.findMatrixWithSortedDescActors().getActors()).extracting("person") + .extracting("name") + .containsExactly("Laurence Fishburne", "Keanu Reeves", "Hugo Weaving", "Gloria Foster", "Emil Eifrem", + "Carrie-Anne Moss"); } - @Test // GH-2470 void mapPathWithSingleNode(@Autowired MovieRepository repository) { assertThat(repository.findSingleNodeWithPath()).isNotNull(); @@ -512,7 +418,9 @@ class AdvancedMappingIT { Movie movie = new Movie("Test", "Movie"); neo4jTemplate.saveAs(movie, MovieWithMovieList.class); - Movie foundMovie = neo4jTemplate.findOne("MATCH (m:Movie{title:'Test'}) return m", Collections.emptyMap(), Movie.class).get(); + Movie foundMovie = neo4jTemplate + .findOne("MATCH (m:Movie{title:'Test'}) return m", Collections.emptyMap(), Movie.class) + .get(); assertThat(foundMovie.getTitle()).isEqualTo("Test"); assertThat(foundMovie.getDescription()).isNull(); @@ -520,31 +428,145 @@ class AdvancedMappingIT { neo4jTemplate.deleteById("Test", Movie.class); } + interface MovieProjectionWithActorProjection { + + String getTitle(); + + List getActors(); + + interface ActorProjection { + + List getRoles(); + + PersonProjection getPerson(); + + interface PersonProjection { + + String getName(); + + List getActedIn(); + + } + + } + + } + + interface MovieProjection { + + String getTitle(); + + List getActors(); + + } + + interface MovieWithSequelProjection { + + String getTitle(); + + MovieWithSequelProjection getSequel(); + + } + + interface MovieWithSequelEntity { + + String getTitle(); + + Movie getSequel(); + + } + + interface MovieWithMovieList { + + String getTitle(); + + List getSequel(); + + } + + interface MovieRepository extends Neo4jRepository { + + MovieProjection findProjectionByTitle(String title); + + MovieDTO findDTOByTitle(String title); + + MovieProjectionWithActorProjection findProjectionWithProjectionByTitle(@Param("title") String title); + + @Query("MATCH p=(movie:Movie)<-[r:ACTED_IN]-(n:Person) WHERE movie.title=$title RETURN collect(p)") + Movie customPathQueryMovieFind(@Param("title") String title); + + @Query("MATCH p=(movie:Movie)<-[r:ACTED_IN]-(n:Person) WHERE movie.title=$title RETURN collect(p)") + List customPathQueryMoviesFind(@Param("title") String title); + + MovieWithSequelProjection findProjectionByTitleAndDescription(String title, String description); + + MovieWithSequelEntity findByTitleAndDescription(String title, String description); + + @Query("MATCH (m:Movie{title:'The Matrix'})<-[a:ACTED_IN]-(p:Person) WITH a,p,m order by p.name return m, collect(a), collect(p)") + Movie findMatrixWithSortedAscActors(); + + @Query("MATCH (m:Movie{title:'The Matrix'})<-[a:ACTED_IN]-(p:Person) WITH a,p,m order by p.name DESC return m, collect(a), collect(p)") + Movie findMatrixWithSortedDescActors(); + + @Query("MATCH p=(:Movie{title:'The Matrix'}) return p") + Movie findSingleNodeWithPath(); + + @Query("MATCH p=(m:Movie{title:'The Matrix'})<-[:ACTED_IN]-(:Person) return m, collect(nodes(p)), collect(relationships(p))") + List findMultipleSameTypeRelationshipsWithPath(); + + } + + static class MovieDTO { + + private final String title; + + private final List actors; + + MovieDTO(String title, List actors) { + this.title = title; + this.actors = actors; + } + + String getTitle() { + return this.title; + } + + List getActors() { + return this.actors; + } + + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/reactive/ReactiveAdvancedMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/movies/reactive/ReactiveAdvancedMappingIT.java index 20e470197..008ef2cfd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/reactive/ReactiveAdvancedMappingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/reactive/ReactiveAdvancedMappingIT.java @@ -15,11 +15,26 @@ */ package org.springframework.data.neo4j.integration.movies.reactive; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,20 +56,6 @@ import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.repository.query.Param; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -78,12 +79,350 @@ class ReactiveAdvancedMappingIT { } } + @Test + void cyclicMappingShouldReturnResultForFindById(@Autowired MovieRepository repository) { + StepVerifier.create(repository.findById("The Matrix")).assertNext(movie -> { + assertThat(movie).isNotNull(); + assertThat(movie.getTitle()).isEqualTo("The Matrix"); + assertThat(movie.getActors()).hasSize(6); + }).verifyComplete(); + } + + @Test + void cyclicMappingShouldReturnResultForFindAllById(@Autowired MovieRepository repository) { + StepVerifier + .create(repository + .findAllById(Arrays.asList("The Matrix", "The Matrix Revolutions", "The Matrix Reloaded"))) + .expectNextCount(3) + .verifyComplete(); + } + + @Test + void cyclicMappingShouldReturnResultForFindAll(@Autowired MovieRepository repository) { + StepVerifier.create(repository.findAll()).expectNextCount(38).verifyComplete(); + } + + @Test // GH-2117 + void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjections(@Autowired MovieRepository movieRepository) { + + // The movie domain is a good fit for this test + // as the cyclic dependencies is pretty slow to retrieve from Neo4j + // this does OOM in most setups. + StepVerifier.create(movieRepository.findProjectionByTitle("The Matrix")).assertNext(projection -> { + assertThat(projection.getTitle()).isNotNull(); + assertThat(projection.getActors()).isNotEmpty(); + }).verifyComplete(); + } + + @Test // GH-2117 + void bothCyclicAndNonCyclicRelationshipsAreExcludedFromDTOProjections(@Autowired MovieRepository movieRepository) { + + // The movie domain is a good fit for this test + // as the cyclic dependencies is pretty slow to retrieve from Neo4j + // this does OOM in most setups. + StepVerifier.create(movieRepository.findDTOByTitle("The Matrix")).assertNext(dtoProjection -> { + assertThat(dtoProjection.getTitle()).isNotNull(); + assertThat(dtoProjection.getActors()).extracting("name") + .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", + "Carrie-Anne Moss", "Hugo Weaving"); + }).verifyComplete(); + } + + @Test // GH-2117 + void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjectionsWithProjections( + @Autowired MovieRepository movieRepository) { + + // The movie domain is a good fit for this test + // as the cyclic dependencies is pretty slow to retrieve from Neo4j + // this does OOM in most setups. + StepVerifier.create(movieRepository.findProjectionWithProjectionByTitle("The Matrix")) + .assertNext(projection -> { + assertThat(projection.getTitle()).isEqualTo("The Matrix"); + assertThat(projection.getActors()).extracting("person") + .extracting("name") + .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", + "Carrie-Anne Moss", "Hugo Weaving"); + assertThat(projection.getActors()).flatExtracting("roles") + .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); + }) + .verifyComplete(); + } + + @Test // GH-2114 + void bothStartAndEndNodeOfPathsMustBeLookedAt(@Autowired ReactiveNeo4jTemplate template) { + + // @ParameterizedTest does not work together with the parameter resolver for + // @Autowired + for (String query : new String[] { "MATCH p=()-[:IS_SIBLING_OF]-> () RETURN p", + "MATCH (s)-[:IS_SIBLING_OF]-> (e) RETURN [s,e]" }) { + StepVerifier.create(template.findAll(query, Collections.emptyMap(), Person.class)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .consumeRecordedWith(people -> assertThat(people).extracting(Person::getName) + .containsExactlyInAnyOrder("Lilly Wachowski", "Lana Wachowski")) + .verifyComplete(); + } + } + + @Test // GH-2114 + void directionAndTypeLessPathMappingShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + StepVerifier + .create(template.findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), Person.class)) + .expectNextCount(6) + .verifyComplete(); + } + + @Test // GH-2114 + void mappingOfAPathWithOddNumberOfElementsShouldWorkFromStartToEnd(@Autowired ReactiveNeo4jTemplate template) { + + StepVerifier.create(template.findAll(""" + MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) + WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') + RETURN p""", Collections.emptyMap(), Movie.class)) + .recordWith(ArrayList::new) + .expectNextCount(3) + .consumeRecordedWith(result -> { + Map movies = result.stream() + .collect(Collectors.toMap(Movie::getTitle, Function.identity())); + + // This is the actual test for the original issue… When the end node of a + // segment is not taken into account, Emil is not an actor + assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); + assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", + m -> assertThat(m.getActors()).isNotEmpty()); + + assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getSequel()).isNotNull()); + assertThat(movies).hasEntrySatisfying("The Matrix Reloaded", + m -> assertThat(m.getSequel()).isNotNull()); + assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", + m -> assertThat(m.getSequel()).isNull()); + }) + .verifyComplete(); + } + + @Test // GH-2114 + void mappingOfAPathWithEventNumberOfElementsShouldWorkFromStartToEnd(@Autowired ReactiveNeo4jTemplate template) { + + StepVerifier.create(template.findAll(""" + MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) + WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') + RETURN p""", Collections.emptyMap(), Movie.class)) + .recordWith(ArrayList::new) + .expectNextCount(3) + .consumeRecordedWith(result -> { + Map movies = result.stream() + .collect(Collectors.toMap(Movie::getTitle, Function.identity())); + + // This is the actual test for the original issue… When the end node of a + // segment is not taken into account, Emil is not an actor + assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); + assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", + m -> assertThat(m.getActors()).isEmpty()); + + assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getSequel()).isNotNull()); + assertThat(movies).hasEntrySatisfying("The Matrix Reloaded", + m -> assertThat(m.getSequel()).isNotNull()); + assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", + m -> assertThat(m.getSequel()).isNull()); + }) + .verifyComplete(); + } + + /** + * Here all paths are going into multiple records. Each path will be one record. The + * elements in the path will be seen as aggregated on the server side and each of the + * aggregates will also be aggregated. + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void multiplePathsShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + String cypherQuery = "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "RETURN allPaths"; + + StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) + .expectNextCount(7) + .verifyComplete(); + } + + /** + * Here all paths are going into one single record. + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void multiplePreAggregatedPathsShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + String cypherQuery = "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "RETURN collect(allPaths)"; + + StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) + .expectNextCount(7) + .verifyComplete(); + } + + /** + * This tests checks whether all nodes that fit a certain class along a path are + * mapped correctly. + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void pathMappingWithoutAdditionalInformationShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + parameters.put("requiredMovie", "The Da Vinci Code"); + String cypherQuery = "MATCH p=shortestPath((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "WHERE size([n IN nodes(p) WHERE n.title = $requiredMovie]) > 0\n" + "RETURN p"; + StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) + .recordWith(ArrayList::new) + .expectNextCount(4) + .consumeRecordedWith(people -> { + assertThat(people).hasSize(4) + .extracting(Person::getName) + .contains("Kevin Bacon", "Jessica Thompson", "Angela Scope"); // Two + // paths + // lead + // there, + // one + // with + // Ron + // Howard, + // one + // with + // Tom + // Hanks. + assertThat(people).element(2) + .extracting(Person::getReviewed) + .satisfies(movies -> assertThat(movies).extracting(Movie::getTitle) + .containsExactly("The Da Vinci Code")); + }) + .verifyComplete(); + } + + /** + * This tests checks whether all nodes that fit a certain class along a path are + * mapped correctly and if the additional joined information is applied as well. + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void pathMappingWithAdditionalInformationShouldWork(@Autowired ReactiveNeo4jTemplate template) { + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Meg Ryan"); + parameters.put("requiredMovie", "The Da Vinci Code"); + String cypherQuery = "MATCH p=shortestPath(\n" + + "(p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "WITH p, [n in nodes(p) WHERE n:Movie] as mn\n" + "UNWIND mn as m\n" + + "MATCH (m) <-[r:DIRECTED]- (d:Person)\n" + "RETURN p, collect(r), collect(d)"; + StepVerifier.create(template.findAll(cypherQuery, parameters, Movie.class)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .consumeRecordedWith(movies -> assertThat(movies).hasSize(2) + .allSatisfy(m -> assertThat(m.getDirectors()).isNotEmpty()) + .first() + .satisfies(m -> assertThat(m.getDirectors()).extracting(Person::getName) + .containsAnyOf("Ron Howard", "Rob Reiner"))) + .verifyComplete(); + } + + /** + * This tests checks if the result of a custom path based query will get mapped + * correctly to an instance of the defined type instead of a collection. + */ + @Test // DATAGRAPH-2107 + void customPathMappingResultsInScalarResultIfDefined(@Autowired MovieRepository movieRepository) { + StepVerifier.create(movieRepository.customPathQueryMovieFind("The Matrix Revolutions")).assertNext(movie -> { + assertThat(movie).isNotNull(); + assertThat(movie.getActors()).hasSize(5); + }).verifyComplete(); + } + + /** + * This tests checks if the result of a custom path based query will get mapped + * correctly to a collection of the defined type with all the fields hydrated. + */ + @Test // DATAGRAPH-2109 + void customPathMappingCollectionResultsInHydratedEntities(@Autowired MovieRepository movieRepository) { + StepVerifier.create(movieRepository.customPathQueryMoviesFind("The Matrix Revolutions")) + .assertNext(movie -> assertThat(movie.getActors()).hasSize(5)) + .verifyComplete(); + } + + @Test // GH-2117 updated for GH-2320 + void cyclicRelationshipsShouldHydrateCorrectlyProjectionsWithProjections( + @Autowired MovieRepository movieRepository) { + + // The movie domain is a good fit for this test + // as the cyclic dependencies is pretty slow to retrieve from Neo4j + // this does OOM in most setups. + StepVerifier.create(movieRepository.findProjectionWithProjectionByTitle("The Matrix")) + .assertNext(projection -> { + assertThat(projection.getTitle()).isNotNull(); + assertThat(projection.getActors()).extracting("person") + .extracting("name") + .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", + "Carrie-Anne Moss", "Hugo Weaving"); + assertThat(projection.getActors()).flatExtracting("roles") + .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); + + // second level mapping of entity cycle + assertThat(projection.getActors()).extracting("person") + .allMatch(person -> !((MovieProjectionWithActorProjection.ActorProjection.PersonProjection) person) + .getActedIn() + .isEmpty()); + + // n+1 level mapping of entity cycle + assertThat(projection.getActors()).extracting("person") + .flatExtracting("actedIn") + .extracting("directors") + .allMatch(directors -> !((Collection) directors).isEmpty()); + }) + .verifyComplete(); + } + + @Test // GH-2320 + void projectDirectCycleProjectionReference(@Autowired MovieRepository movieRepository) { + StepVerifier + .create(movieRepository.findProjectionByTitleAndDescription("The Matrix", "Welcome to the Real World")) + .assertNext(movie -> { + assertThat(movie.getSequel().getTitle()).isEqualTo("The Matrix Reloaded"); + assertThat(movie.getSequel().getSequel().getTitle()).isEqualTo("The Matrix Revolutions"); + }) + .verifyComplete(); + } + + @Test // GH-2320 + void projectDirectCycleEntityReference(@Autowired MovieRepository movieRepository) { + StepVerifier.create(movieRepository.findByTitleAndDescription("The Matrix", "Welcome to the Real World")) + .assertNext(movie -> { + + Movie firstSequel = movie.getSequel(); + assertThat(firstSequel.getTitle()).isEqualTo("The Matrix Reloaded"); + assertThat(firstSequel.getActors()).isNotEmpty(); + + Movie secondSequel = firstSequel.getSequel(); + assertThat(secondSequel.getTitle()).isEqualTo("The Matrix Revolutions"); + assertThat(secondSequel.getActors()).isNotEmpty(); + }) + .verifyComplete(); + } + interface MovieProjectionWithActorProjection { + String getTitle(); List getActors(); interface ActorProjection { + List getRoles(); MovieProjectionWithActorProjection.ActorProjection.PersonProjection getPerson(); @@ -93,8 +432,11 @@ class ReactiveAdvancedMappingIT { String getName(); List getActedIn(); + } + } + } interface MovieProjection { @@ -102,36 +444,23 @@ class ReactiveAdvancedMappingIT { String getTitle(); List getActors(); - } - static class MovieDTO { - - private final String title; - - private final List actors; - - MovieDTO(String title, List actors) { - this.title = title; - this.actors = actors; - } - - public String getTitle() { - return title; - } - - public List getActors() { - return actors; - } } interface MovieWithSequelProjection { + String getTitle(); + MovieWithSequelProjection getSequel(); + } interface MovieWithSequelEntity { + String getTitle(); + Movie getSequel(); + } interface MovieRepository extends ReactiveNeo4jRepository { @@ -151,339 +480,28 @@ class ReactiveAdvancedMappingIT { Mono findProjectionByTitleAndDescription(String title, String description); Mono findByTitleAndDescription(String title, String description); + } - @Test - void cyclicMappingShouldReturnResultForFindById(@Autowired MovieRepository repository) { - StepVerifier.create(repository.findById("The Matrix")) - .assertNext(movie -> { - assertThat(movie).isNotNull(); - assertThat(movie.getTitle()).isEqualTo("The Matrix"); - assertThat(movie.getActors()).hasSize(6); - }) - .verifyComplete(); - } + static class MovieDTO { - @Test - void cyclicMappingShouldReturnResultForFindAllById(@Autowired MovieRepository repository) { - StepVerifier.create(repository.findAllById(Arrays.asList("The Matrix", "The Matrix Revolutions", "The Matrix Reloaded"))) - .expectNextCount(3) - .verifyComplete(); - } + private final String title; - @Test - void cyclicMappingShouldReturnResultForFindAll(@Autowired MovieRepository repository) { - StepVerifier.create(repository.findAll()) - .expectNextCount(38) - .verifyComplete(); - } + private final List actors; - @Test // GH-2117 - void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjections(@Autowired MovieRepository movieRepository) { - - // The movie domain is a good fit for this test - // as the cyclic dependencies is pretty slow to retrieve from Neo4j - // this does OOM in most setups. - StepVerifier.create(movieRepository.findProjectionByTitle("The Matrix")) - .assertNext(projection -> { - assertThat(projection.getTitle()).isNotNull(); - assertThat(projection.getActors()).isNotEmpty(); - }) - .verifyComplete(); - } - - @Test // GH-2117 - void bothCyclicAndNonCyclicRelationshipsAreExcludedFromDTOProjections(@Autowired MovieRepository movieRepository) { - - // The movie domain is a good fit for this test - // as the cyclic dependencies is pretty slow to retrieve from Neo4j - // this does OOM in most setups. - StepVerifier.create(movieRepository.findDTOByTitle("The Matrix")) - .assertNext(dtoProjection -> { - assertThat(dtoProjection.getTitle()).isNotNull(); - assertThat(dtoProjection.getActors()).extracting("name") - .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", - "Carrie-Anne Moss", "Hugo Weaving"); - }) - .verifyComplete(); - } - - @Test // GH-2117 - void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjectionsWithProjections(@Autowired MovieRepository movieRepository) { - - // The movie domain is a good fit for this test - // as the cyclic dependencies is pretty slow to retrieve from Neo4j - // this does OOM in most setups. - StepVerifier.create(movieRepository.findProjectionWithProjectionByTitle("The Matrix")) - .assertNext(projection -> { - assertThat(projection.getTitle()).isEqualTo("The Matrix"); - assertThat(projection.getActors()).extracting("person").extracting("name") - .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", - "Carrie-Anne Moss", "Hugo Weaving"); - assertThat(projection.getActors()).flatExtracting("roles") - .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); - }) - .verifyComplete(); - } - - @Test // GH-2114 - void bothStartAndEndNodeOfPathsMustBeLookedAt(@Autowired ReactiveNeo4jTemplate template) { - - // @ParameterizedTest does not work together with the parameter resolver for @Autowired - for (String query : new String[] { - "MATCH p=()-[:IS_SIBLING_OF]-> () RETURN p", - "MATCH (s)-[:IS_SIBLING_OF]-> (e) RETURN [s,e]" - }) { - StepVerifier.create(template.findAll(query, Collections.emptyMap(), Person.class)) - .recordWith(ArrayList::new) - .expectNextCount(2) - .consumeRecordedWith(people -> - assertThat(people).extracting(Person::getName) - .containsExactlyInAnyOrder("Lilly Wachowski", "Lana Wachowski") - ) - .verifyComplete(); + MovieDTO(String title, List actors) { + this.title = title; + this.actors = actors; } - } - @Test // GH-2114 - void directionAndTypeLessPathMappingShouldWork(@Autowired ReactiveNeo4jTemplate template) { + String getTitle() { + return this.title; + } - StepVerifier.create( - template.findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), Person.class)) - .expectNextCount(6) - .verifyComplete(); - } + List getActors() { + return this.actors; + } - @Test // GH-2114 - void mappingOfAPathWithOddNumberOfElementsShouldWorkFromStartToEnd(@Autowired ReactiveNeo4jTemplate template) { - - StepVerifier.create(template - .findAll(""" - MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) - WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') - RETURN p""", Collections.emptyMap(), Movie.class)) - .recordWith(ArrayList::new) - .expectNextCount(3) - .consumeRecordedWith(result -> { - Map movies = result.stream().collect(Collectors.toMap(Movie::getTitle, Function.identity())); - - // This is the actual test for the original issue… When the end node of a segment is not taken into account, Emil is not an actor - assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); - assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getActors()).isNotEmpty()); - - assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getSequel()).isNotNull()); - assertThat(movies).hasEntrySatisfying("The Matrix Reloaded", m -> assertThat(m.getSequel()).isNotNull()); - assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getSequel()).isNull()); - }) - .verifyComplete(); - } - - @Test // GH-2114 - void mappingOfAPathWithEventNumberOfElementsShouldWorkFromStartToEnd(@Autowired ReactiveNeo4jTemplate template) { - - StepVerifier.create(template - .findAll(""" - MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) - WHERE any(t IN [ x in nodes(p) | x.title] WHERE t = 'The Matrix Reloaded') - RETURN p""", Collections.emptyMap(), Movie.class)) - .recordWith(ArrayList::new) - .expectNextCount(3) - .consumeRecordedWith(result -> { - Map movies = result.stream().collect(Collectors.toMap(Movie::getTitle, Function.identity())); - - // This is the actual test for the original issue… When the end node of a segment is not taken into account, Emil is not an actor - assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getActors()).isNotEmpty()); - assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getActors()).isEmpty()); - - assertThat(movies).hasEntrySatisfying("The Matrix", m -> assertThat(m.getSequel()).isNotNull()); - assertThat(movies).hasEntrySatisfying("The Matrix Reloaded", m -> assertThat(m.getSequel()).isNotNull()); - assertThat(movies).hasEntrySatisfying("The Matrix Revolutions", m -> assertThat(m.getSequel()).isNull()); - }) - .verifyComplete(); - } - - /** - * Here all paths are going into multiple records. Each path will be one record. The elements in the path will be - * seen as aggregated on the server side and each of the aggregates will also be aggregated. - * - * @param template Used for querying - */ - @Test // DATAGRAPH-1437 - void multiplePathsShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - Map parameters = new HashMap<>(); - parameters.put("person1", "Kevin Bacon"); - parameters.put("person2", "Angela Scope"); - String cypherQuery = - "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "RETURN allPaths"; - - StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) - .expectNextCount(7) - .verifyComplete(); - } - - /** - * Here all paths are going into one single record. - * - * @param template Used for querying - */ - @Test // DATAGRAPH-1437 - void multiplePreAggregatedPathsShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - Map parameters = new HashMap<>(); - parameters.put("person1", "Kevin Bacon"); - parameters.put("person2", "Angela Scope"); - String cypherQuery = - "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "RETURN collect(allPaths)"; - - StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) - .expectNextCount(7) - .verifyComplete(); - } - - /** - * This tests checks whether all nodes that fit a certain class along a path are mapped correctly. - * - * @param template Used for querying - */ - @Test // DATAGRAPH-1437 - void pathMappingWithoutAdditionalInformationShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - Map parameters = new HashMap<>(); - parameters.put("person1", "Kevin Bacon"); - parameters.put("person2", "Angela Scope"); - parameters.put("requiredMovie", "The Da Vinci Code"); - String cypherQuery = - "MATCH p=shortestPath((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "WHERE size([n IN nodes(p) WHERE n.title = $requiredMovie]) > 0\n" - + "RETURN p"; - StepVerifier.create(template.findAll(cypherQuery, parameters, Person.class)) - .recordWith(ArrayList::new) - .expectNextCount(4) - .consumeRecordedWith(people -> { - assertThat(people) - .hasSize(4) - .extracting(Person::getName) - .contains("Kevin Bacon", "Jessica Thompson", - "Angela Scope"); // Two paths lead there, one with Ron Howard, one with Tom Hanks. - assertThat(people).element(2).extracting(Person::getReviewed) - .satisfies( - movies -> assertThat(movies).extracting(Movie::getTitle).containsExactly("The Da Vinci Code")); - }) - .verifyComplete(); - } - - /** - * This tests checks whether all nodes that fit a certain class along a path are mapped correctly and if the - * additional joined information is applied as well. - * - * @param template Used for querying - */ - @Test // DATAGRAPH-1437 - void pathMappingWithAdditionalInformationShouldWork(@Autowired ReactiveNeo4jTemplate template) { - Map parameters = new HashMap<>(); - parameters.put("person1", "Kevin Bacon"); - parameters.put("person2", "Meg Ryan"); - parameters.put("requiredMovie", "The Da Vinci Code"); - String cypherQuery = - "MATCH p=shortestPath(\n" - + "(p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" - + "WITH p, [n in nodes(p) WHERE n:Movie] as mn\n" - + "UNWIND mn as m\n" - + "MATCH (m) <-[r:DIRECTED]- (d:Person)\n" - + "RETURN p, collect(r), collect(d)"; - StepVerifier.create(template.findAll(cypherQuery, parameters, Movie.class)) - .recordWith(ArrayList::new) - .expectNextCount(2) - .consumeRecordedWith(movies -> - assertThat(movies) - .hasSize(2) - .allSatisfy(m -> assertThat(m.getDirectors()).isNotEmpty()) - .first() - .satisfies(m -> assertThat(m.getDirectors()).extracting(Person::getName) - .containsAnyOf("Ron Howard", "Rob Reiner"))) - .verifyComplete(); - } - - /** - * This tests checks if the result of a custom path based query will get mapped correctly to an instance - * of the defined type instead of a collection. - */ - @Test // DATAGRAPH-2107 - void customPathMappingResultsInScalarResultIfDefined(@Autowired MovieRepository movieRepository) { - StepVerifier.create(movieRepository.customPathQueryMovieFind("The Matrix Revolutions")) - .assertNext(movie -> { - assertThat(movie).isNotNull(); - assertThat(movie.getActors()).hasSize(5); - }) - .verifyComplete(); - } - - /** - * This tests checks if the result of a custom path based query will get mapped correctly to a collection - * of the defined type with all the fields hydrated. - */ - @Test // DATAGRAPH-2109 - void customPathMappingCollectionResultsInHydratedEntities(@Autowired MovieRepository movieRepository) { - StepVerifier.create(movieRepository.customPathQueryMoviesFind("The Matrix Revolutions")) - .assertNext(movie -> assertThat(movie.getActors()).hasSize(5)) - .verifyComplete(); - } - - @Test // GH-2117 updated for GH-2320 - void cyclicRelationshipsShouldHydrateCorrectlyProjectionsWithProjections(@Autowired MovieRepository movieRepository) { - - // The movie domain is a good fit for this test - // as the cyclic dependencies is pretty slow to retrieve from Neo4j - // this does OOM in most setups. - StepVerifier.create(movieRepository.findProjectionWithProjectionByTitle("The Matrix")) - .assertNext(projection -> { - assertThat(projection.getTitle()).isNotNull(); - assertThat(projection.getActors()).extracting("person").extracting("name") - .containsExactlyInAnyOrder("Gloria Foster", "Keanu Reeves", "Emil Eifrem", "Laurence Fishburne", - "Carrie-Anne Moss", "Hugo Weaving"); - assertThat(projection.getActors()).flatExtracting("roles") - .containsExactlyInAnyOrder("The Oracle", "Morpheus", "Trinity", "Agent Smith", "Emil", "Neo"); - - // second level mapping of entity cycle - assertThat(projection.getActors()).extracting("person") - .allMatch(person -> - !((MovieProjectionWithActorProjection.ActorProjection.PersonProjection) person).getActedIn().isEmpty()); - - // n+1 level mapping of entity cycle - assertThat(projection.getActors()).extracting("person").flatExtracting("actedIn").extracting("directors") - .allMatch(directors -> !((Collection) directors).isEmpty()); - }) - .verifyComplete(); - } - - @Test // GH-2320 - void projectDirectCycleProjectionReference(@Autowired MovieRepository movieRepository) { - StepVerifier.create(movieRepository.findProjectionByTitleAndDescription("The Matrix", - "Welcome to the Real World")) - .assertNext(movie -> { - assertThat(movie.getSequel().getTitle()).isEqualTo("The Matrix Reloaded"); - assertThat(movie.getSequel().getSequel().getTitle()).isEqualTo("The Matrix Revolutions"); - }) - .verifyComplete(); - } - - @Test // GH-2320 - void projectDirectCycleEntityReference(@Autowired MovieRepository movieRepository) { - StepVerifier.create(movieRepository.findByTitleAndDescription("The Matrix", "Welcome to the Real World")) - .assertNext(movie -> { - - Movie firstSequel = movie.getSequel(); - assertThat(firstSequel.getTitle()).isEqualTo("The Matrix Reloaded"); - assertThat(firstSequel.getActors()).isNotEmpty(); - - Movie secondSequel = firstSequel.getSequel(); - assertThat(secondSequel.getTitle()).isEqualTo("The Matrix Revolutions"); - assertThat(secondSequel.getActors()).isNotEmpty(); - }) - .verifyComplete(); } @Configuration @@ -492,25 +510,30 @@ class ReactiveAdvancedMappingIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Actor.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Actor.java index 9bf11c4a9..a1d6219e9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Actor.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Actor.java @@ -24,40 +24,38 @@ import org.springframework.data.neo4j.core.schema.TargetNode; /** * @author Michael J. Simons - * @soundtrack Body Count - Manslaughter */ @RelationshipProperties public final class Actor { - @RelationshipId - private Long id; - @TargetNode private final Person person; private final List roles; + @RelationshipId + private Long id; + public Actor(Person person, List roles) { this.person = person; this.roles = roles; } public Person getPerson() { - return person; + return this.person; } public String getName() { - return person.getName(); + return this.person.getName(); } public List getRoles() { - return Collections.unmodifiableList(roles); + return Collections.unmodifiableList(this.roles); } - @Override public String toString() { - return "Actor{" + - "person=" + person + - ", roles=" + roles + - '}'; + @Override + public String toString() { + return "Actor{" + "person=" + this.person + ", roles=" + this.roles + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/CypherUtils.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/CypherUtils.java index d46f7d8d3..070b64f8a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/CypherUtils.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/CypherUtils.java @@ -27,6 +27,9 @@ import org.neo4j.driver.Session; */ public final class CypherUtils { + private CypherUtils() { + } + public static void loadCypherFromResource(String resource, Session session) throws IOException { try (BufferedReader moviesReader = new BufferedReader( new InputStreamReader(CypherUtils.class.getResourceAsStream(resource)))) { @@ -36,6 +39,4 @@ public final class CypherUtils { } } - private CypherUtils() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Movie.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Movie.java index 252429dde..6199e6e62 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Movie.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Movie.java @@ -15,19 +15,18 @@ */ package org.springframework.data.neo4j.integration.movies.shared; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + 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; import org.springframework.data.neo4j.core.schema.Relationship.Direction; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - /** * @author Michael J. Simons - * @soundtrack Body Count - Manslaughter */ @Node public final class Movie { @@ -58,11 +57,11 @@ public final class Movie { } public String getTitle() { - return title; + return this.title; } public String getDescription() { - return description; + return this.description; } public List getActors() { @@ -74,7 +73,7 @@ public final class Movie { } public Integer getReleased() { - return released; + return this.released; } public void setReleased(Integer released) { @@ -82,6 +81,7 @@ public final class Movie { } public Movie getSequel() { - return sequel; + return this.sequel; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Organisation.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Organisation.java index dbbdb5994..0b68f2b05 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Organisation.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Organisation.java @@ -23,25 +23,25 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -/** - * @soundtrack Guns n' Roses - Appetite For Destruction - */ @Node public class Organisation { - @Id - @GeneratedValue - private Long id; private final String partnerCode; + private final String code; + private final String name; + private final String type; @Relationship(type = "CHILD_ORGANISATIONS") private final List organisations; - public Organisation(String partnerCode, String code, String name, String type, - List organisations) { + @Id + @GeneratedValue + private Long id; + + public Organisation(String partnerCode, String code, String name, String type, List organisations) { this.partnerCode = partnerCode; this.code = code; this.name = name; @@ -59,37 +59,35 @@ public class Organisation { } public Long getId() { - return id; + return this.id; } public String getPartnerCode() { - return partnerCode; + return this.partnerCode; } public String getCode() { - return code; + return this.code; } public String getName() { - return name; + return this.name; } public String getType() { - return type; + return this.type; } public List getOrganisations() { - return organisations == null ? Collections.emptyList() : Collections.unmodifiableList(organisations); + return (this.organisations != null) ? Collections.unmodifiableList(this.organisations) + : Collections.emptyList(); } - @Override public String toString() { - return "Organisation{" + - "id=" + id + - ", partnerCode='" + partnerCode + '\'' + - ", code='" + code + '\'' + - ", name='" + name + '\'' + - ", type='" + type + '\'' + - ", organisations=" + organisations + - '}'; + @Override + public String toString() { + return "Organisation{" + "id=" + this.id + ", partnerCode='" + this.partnerCode + '\'' + ", code='" + this.code + + '\'' + ", name='" + this.name + '\'' + ", type='" + this.type + '\'' + ", organisations=" + + this.organisations + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Partner.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Partner.java index 815808518..298e5005f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Partner.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Partner.java @@ -23,21 +23,20 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -/** - * @soundtrack Guns n' Roses - Appetite For Destruction - */ @Node public class Partner { - @Id - @GeneratedValue - private Long id; private final String code; + private final String name; @Relationship(type = "CHILD_ORGANISATIONS") private final List organisations; + @Id + @GeneratedValue + private Long id; + public Partner(String code, String name, List organisations) { this.code = code; this.name = name; @@ -55,27 +54,26 @@ public class Partner { } public Long getId() { - return id; + return this.id; } public String getCode() { - return code; + return this.code; } public String getName() { - return name; + return this.name; } public List getOrganisations() { - return organisations == null ? Collections.emptyList() : Collections.unmodifiableList(organisations); + return (this.organisations != null) ? Collections.unmodifiableList(this.organisations) + : Collections.emptyList(); } - @Override public String toString() { - return "Partner{" + - "id=" + id + - ", code='" + code + '\'' + - ", name='" + name + '\'' + - ", organisations=" + organisations + - '}'; + @Override + public String toString() { + return "Partner{" + "id=" + this.id + ", code='" + this.code + '\'' + ", name='" + this.name + '\'' + + ", organisations=" + this.organisations + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Person.java b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Person.java index db675ef3a..e59ff6d53 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Person.java +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/shared/Person.java @@ -26,12 +26,12 @@ import org.springframework.data.neo4j.core.schema.Relationship; /** * @author Michael J. Simons - * @soundtrack Body Count - Manslaughter */ @Node public final class Person { - @Id @GeneratedValue + @Id + @GeneratedValue private final Long id; private final String name; @@ -56,15 +56,15 @@ public final class Person { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public Integer getBorn() { - return born; + return this.born; } public void setBorn(Integer born) { @@ -72,14 +72,12 @@ public final class Person { } public List getReviewed() { - return reviewed; + return this.reviewed; } - @Override public String toString() { - return "Person{" + - "id=" + id + - ", name='" + name + '\'' + - ", born=" + born + - '}'; + @Override + public String toString() { + return "Person{" + "id=" + this.id + ", name='" + this.name + '\'' + ", born=" + this.born + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/MultipleContextsIT.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/MultipleContextsIT.java index 81a3fae49..5e17e8733 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/MultipleContextsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/MultipleContextsIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.multiple_ctx_imperative; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import org.junit.jupiter.api.Tag; @@ -27,6 +25,10 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logging; import org.neo4j.driver.Session; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.integration.multiple_ctx_imperative.domain1.Domain1Config; import org.springframework.data.neo4j.integration.multiple_ctx_imperative.domain1.Domain1Entity; @@ -38,9 +40,8 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.testcontainers.containers.Neo4jContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; /** * Tests whether multiple context are truly separated. @@ -53,12 +54,10 @@ import org.testcontainers.junit.jupiter.Testcontainers; public class MultipleContextsIT { @Container - private static Neo4jContainer container1 = new Neo4jContainer<>("neo4j:5") - .withAdminPassword("verysecret1"); + private static Neo4jContainer container1 = new Neo4jContainer<>("neo4j:5").withAdminPassword("verysecret1"); @Container - private static Neo4jContainer container2 = new Neo4jContainer<>("neo4j:5") - .withAdminPassword("verysecret2"); + private static Neo4jContainer container2 = new Neo4jContainer<>("neo4j:5").withAdminPassword("verysecret2"); @DynamicPropertySource static void neo4jSettings(DynamicPropertyRegistry registry) { @@ -70,11 +69,34 @@ public class MultipleContextsIT { registry.add("database2.password", () -> "verysecret2"); } + /** + * Create drivers independend from the setup under test. + * @param boltUrl Where to connect to + * @param password Which password + * @return Minimal driver instance. + */ + private static Driver newDriver(String boltUrl, String password) { + + Config driverConfig = Config.builder() + .withMaxConnectionPoolSize(1) + .withLogging(Logging.none()) + .withEventLoopThreads(1) + .build(); + return GraphDatabase.driver(boltUrl, AuthTokens.basic("neo4j", password), driverConfig); + } + + private static void verifyExistenceAndVersion(long id1, Session session) { + Long version = session.executeRead( + tx -> tx.run("MATCH (n) WHERE id(n) = $id RETURN n.version", Collections.singletonMap("id", id1)) + .single() + .get(0) + .asLong()); + assertThat(version).isOne(); + } + @Test // DATAGRAPH-1441 - void repositoriesShouldTargetTheCorrectDatabase( - @Autowired Domain1Repository repo1, - @Autowired Domain2Repository repo2 - ) { + void repositoriesShouldTargetTheCorrectDatabase(@Autowired Domain1Repository repo1, + @Autowired Domain2Repository repo2) { Domain1Entity newEntity1 = repo1.save(new Domain1Entity("For domain 1")); newEntity1.setAnAttribute(newEntity1.getAnAttribute() + " updated"); @@ -97,27 +119,4 @@ public class MultipleContextsIT { } } - /** - * Create drivers independend from the setup under test. - * - * @param boltUrl Where to connect to - * @param password Which password - * @return Minimal driver instance. - */ - private static Driver newDriver(String boltUrl, String password) { - - Config driverConfig = Config.builder() - .withMaxConnectionPoolSize(1) - .withLogging(Logging.none()) - .withEventLoopThreads(1) - .build(); - return GraphDatabase.driver(boltUrl, AuthTokens.basic("neo4j", password), driverConfig); - } - - private static void verifyExistenceAndVersion(long id1, Session session) { - Long version = session - .executeRead(tx -> tx.run("MATCH (n) WHERE id(n) = $id RETURN n.version", Collections - .singletonMap("id", id1)).single().get(0).asLong()); - assertThat(version).isOne(); - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/SharedConfig.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/SharedConfig.java index fc6ec66f3..5b537f0ff 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/SharedConfig.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/SharedConfig.java @@ -22,7 +22,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author Michael J. Simons - * @soundtrack Kelis - Tasty */ @EnableTransactionManagement @Configuration(proxyBeanMethods = false) @@ -32,4 +31,5 @@ public class SharedConfig { public Neo4jConversions neo4jConversions() { return new Neo4jConversions(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Config.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Config.java index afebc2fbd..d40475670 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Config.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Config.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.multiple_ctx_imperative.domai import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,56 +38,54 @@ import org.springframework.transaction.PlatformTransactionManager; /** * @author Michael J. Simons - * @soundtrack Kelis - Tasty */ @Configuration(proxyBeanMethods = false) -@EnableNeo4jRepositories( - basePackageClasses = Domain1Config.class, - neo4jMappingContextRef = "domain1Context", - neo4jTemplateRef = "domain1Template", - transactionManagerRef = "domain1Manager" -) +@EnableNeo4jRepositories(basePackageClasses = Domain1Config.class, neo4jMappingContextRef = "domain1Context", + neo4jTemplateRef = "domain1Template", transactionManagerRef = "domain1Manager") public class Domain1Config { - @Primary @Bean + @Primary + @Bean public Driver domain1Driver(Environment env) { return GraphDatabase.driver(env.getRequiredProperty("database1.url"), AuthTokens.basic("neo4j", env.getRequiredProperty("database1.password"))); } - @Primary @Bean + @Primary + @Bean public Neo4jClient domain1Client(@Qualifier("domain1Driver") Driver driver) { return Neo4jClient.create(driver); } - @Primary @Bean - public Neo4jOperations domain1Template( - @Qualifier("domain1Client") Neo4jClient domain1Client, + @Primary + @Bean + public Neo4jOperations domain1Template(@Qualifier("domain1Client") Neo4jClient domain1Client, @Qualifier("domain1Context") Neo4jMappingContext domain1Context, - @Qualifier("domain1Manager") PlatformTransactionManager domain1TransactionManager - ) { + @Qualifier("domain1Manager") PlatformTransactionManager domain1TransactionManager) { return new Neo4jTemplate(domain1Client, domain1Context, domain1TransactionManager); } - @Primary @Bean - public PlatformTransactionManager domain1Manager( - @Qualifier("domain1Driver") Driver driver, - @Qualifier("domain1Selection") DatabaseSelectionProvider domain1Selection - ) { + @Primary + @Bean + public PlatformTransactionManager domain1Manager(@Qualifier("domain1Driver") Driver driver, + @Qualifier("domain1Selection") DatabaseSelectionProvider domain1Selection) { return new Neo4jTransactionManager(driver, domain1Selection); } - @Primary @Bean + @Primary + @Bean public DatabaseSelectionProvider domain1Selection() { return DatabaseSelection::undecided; } - @Primary @Bean + @Primary + @Bean public Neo4jMappingContext domain1Context(Neo4jConversions neo4jConversions) throws ClassNotFoundException { Neo4jMappingContext context = new Neo4jMappingContext(neo4jConversions); context.setInitialEntitySet(Neo4jEntityScanner.get().scan(this.getClass().getPackage().getName())); context.setStrict(true); return context; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Entity.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Entity.java index c0e1741bc..e1f5f1847 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Entity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Entity.java @@ -22,12 +22,12 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons - * @soundtrack Kelis - Tasty */ @Node public class Domain1Entity { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @Version @@ -40,11 +40,11 @@ public class Domain1Entity { } public Long getId() { - return id; + return this.id; } public Long getVersion() { - return version; + return this.version; } public void setVersion(Long version) { @@ -52,10 +52,11 @@ public class Domain1Entity { } public String getAnAttribute() { - return anAttribute; + return this.anAttribute; } public void setAnAttribute(String anAttribute) { this.anAttribute = anAttribute; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Repository.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Repository.java index 0dc51af4c..442cd8c65 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain1/Domain1Repository.java @@ -19,7 +19,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; /** * @author Michael J. Simons - * @soundtrack Various - T2 Trainspotting */ public interface Domain1Repository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Config.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Config.java index 4ddd347eb..73cd1714e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Config.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Config.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.multiple_ctx_imperative.domai import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,15 +37,10 @@ import org.springframework.transaction.PlatformTransactionManager; /** * @author Michael J. Simons - * @soundtrack Kelis - Tasty */ @Configuration(proxyBeanMethods = false) -@EnableNeo4jRepositories( - basePackageClasses = Domain2Config.class, - neo4jMappingContextRef = "domain2Context", - neo4jTemplateRef = "domain2Template", - transactionManagerRef = "domain2Manager" -) +@EnableNeo4jRepositories(basePackageClasses = Domain2Config.class, neo4jMappingContextRef = "domain2Context", + neo4jTemplateRef = "domain2Template", transactionManagerRef = "domain2Manager") public class Domain2Config { @Bean @@ -60,19 +56,15 @@ public class Domain2Config { } @Bean - public Neo4jOperations domain2Template( - @Qualifier("domain2Client") Neo4jClient domain2Client, + public Neo4jOperations domain2Template(@Qualifier("domain2Client") Neo4jClient domain2Client, @Qualifier("domain2Context") Neo4jMappingContext domain2Context, - @Qualifier("domain2Manager") PlatformTransactionManager domain2TransactionManager - ) { + @Qualifier("domain2Manager") PlatformTransactionManager domain2TransactionManager) { return new Neo4jTemplate(domain2Client, domain2Context, domain2TransactionManager); } @Bean - public PlatformTransactionManager domain2Manager( - @Qualifier("domain2Driver") Driver driver, - @Qualifier("domain2Selection") DatabaseSelectionProvider domain2Selection - ) { + public PlatformTransactionManager domain2Manager(@Qualifier("domain2Driver") Driver driver, + @Qualifier("domain2Selection") DatabaseSelectionProvider domain2Selection) { return new Neo4jTransactionManager(driver, domain2Selection); } @@ -88,4 +80,5 @@ public class Domain2Config { context.setStrict(true); return context; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Entity.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Entity.java index 14033e37b..99142ddcd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Entity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Entity.java @@ -22,12 +22,12 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons - * @soundtrack Kelis - Tasty */ @Node public class Domain2Entity { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @Version @@ -40,18 +40,19 @@ public class Domain2Entity { } public Long getId() { - return id; + return this.id; } public Long getVersion() { - return version; + return this.version; } public String getAnAttribute() { - return anAttribute; + return this.anAttribute; } public void setAnAttribute(String anAttribute) { this.anAttribute = anAttribute; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Repository.java b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Repository.java index c41b884f4..3fcb3efca 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Repository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/multiple_ctx_imperative/domain2/Domain2Repository.java @@ -19,7 +19,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; /** * @author Michael J. Simons - * @soundtrack Various - T2 Trainspotting */ public interface Domain2Repository extends Neo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/properties/DomainClasses.java b/src/test/java/org/springframework/data/neo4j/integration/properties/DomainClasses.java index bde599dbb..199daeca3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/properties/DomainClasses.java +++ b/src/test/java/org/springframework/data/neo4j/integration/properties/DomainClasses.java @@ -15,6 +15,12 @@ */ package org.springframework.data.neo4j.integration.properties; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -26,15 +32,8 @@ import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.neo4j.core.support.DateLong; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * @author Michael J. Simons - * @soundtrack Metallica - Metallica */ final class DomainClasses { @@ -45,128 +44,138 @@ final class DomainClasses { private String knownProperty; - public String getKnownProperty() { + String getKnownProperty() { return this.knownProperty; } - public void setKnownProperty(String knownProperty) { + void setKnownProperty(String knownProperty) { this.knownProperty = knownProperty; } + } @Node static class IrrelevantSourceContainer { - @Id - @GeneratedValue - private Long id; @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") RelationshipPropertyContainer relationshipPropertyContainer; - IrrelevantSourceContainer( - RelationshipPropertyContainer relationshipPropertyContainer) { + @Id + @GeneratedValue + private Long id; + + IrrelevantSourceContainer(RelationshipPropertyContainer relationshipPropertyContainer) { this.relationshipPropertyContainer = relationshipPropertyContainer; } - public Long getId() { + Long getId() { return this.id; } - public RelationshipPropertyContainer getRelationshipPropertyContainer() { - return this.relationshipPropertyContainer; - } - - public void setId(Long id) { + void setId(Long id) { this.id = id; } - public void setRelationshipPropertyContainer(RelationshipPropertyContainer relationshipPropertyContainer) { + RelationshipPropertyContainer getRelationshipPropertyContainer() { + return this.relationshipPropertyContainer; + } + + void setRelationshipPropertyContainer(RelationshipPropertyContainer relationshipPropertyContainer) { this.relationshipPropertyContainer = relationshipPropertyContainer; } + } @Node static class DynRelSourc1 { - @Id - @GeneratedValue - private Long id; @Relationship Map> rels = new HashMap<>(); - public Long getId() { + @Id + @GeneratedValue + private Long id; + + Long getId() { return this.id; } - public Map> getRels() { - return this.rels; - } - - public void setId(Long id) { + void setId(Long id) { this.id = id; } - public void setRels(Map> rels) { + Map> getRels() { + return this.rels; + } + + void setRels(Map> rels) { this.rels = rels; } + } @Node static class DynRelSourc2 { - @Id - @GeneratedValue - private Long id; @Relationship Map rels = new HashMap<>(); - public Long getId() { + @Id + @GeneratedValue + private Long id; + + Long getId() { return this.id; } - public Map getRels() { - return this.rels; - } - - public void setId(Long id) { + void setId(Long id) { this.id = id; } - public void setRels(Map rels) { + Map getRels() { + return this.rels; + } + + void setRels(Map rels) { this.rels = rels; } + } @Node static class IrrelevantTargetContainer { + @Id @GeneratedValue private Long id; + } @RelationshipProperties static class RelationshipPropertyContainer extends BaseClass { - private @RelationshipId Long id; + @RelationshipId + private Long id; @TargetNode private IrrelevantTargetContainer irrelevantTargetContainer; - public Long getId() { + Long getId() { return this.id; } - public IrrelevantTargetContainer getIrrelevantTargetContainer() { - return this.irrelevantTargetContainer; - } - - public void setId(Long id) { + void setId(Long id) { this.id = id; } - public void setIrrelevantTargetContainer(IrrelevantTargetContainer irrelevantTargetContainer) { + IrrelevantTargetContainer getIrrelevantTargetContainer() { + return this.irrelevantTargetContainer; + } + + void setIrrelevantTargetContainer(IrrelevantTargetContainer irrelevantTargetContainer) { this.irrelevantTargetContainer = irrelevantTargetContainer; } + } @Node @@ -176,13 +185,14 @@ final class DomainClasses { @GeneratedValue private Long id; - public Long getId() { + Long getId() { return this.id; } - public void setId(Long id) { + void setId(Long id) { this.id = id; } + } @Node @@ -191,13 +201,14 @@ final class DomainClasses { @Version private Long version; - public Long getVersion() { + Long getVersion() { return this.version; } - public void setVersion(Long version) { + void setVersion(Long version) { this.version = version; } + } @Node @@ -206,13 +217,14 @@ final class DomainClasses { @Id private String id; - public String getId() { + String getId() { return this.id; } - public void setId(String id) { + void setId(String id) { this.id = id; } + } @Node @@ -221,53 +233,52 @@ final class DomainClasses { @Version private Long version; - public Long getVersion() { + Long getVersion() { return this.version; } - public void setVersion(Long version) { + void setVersion(Long version) { this.version = version; } + } @Node static class WeirdSource { + @Relationship(type = "ITS_COMPLICATED") + IrrelevantTargetContainer irrelevantTargetContainer; + @Id @Property("id") @DateLong private Date myFineId; - @Relationship(type = "ITS_COMPLICATED") - IrrelevantTargetContainer irrelevantTargetContainer; - WeirdSource(Date myFineId, IrrelevantTargetContainer irrelevantTargetContainer) { this.myFineId = myFineId; this.irrelevantTargetContainer = irrelevantTargetContainer; } - public Date getMyFineId() { + Date getMyFineId() { return this.myFineId; } - public IrrelevantTargetContainer getIrrelevantTargetContainer() { - return this.irrelevantTargetContainer; - } - - public void setMyFineId(Date myFineId) { + void setMyFineId(Date myFineId) { this.myFineId = myFineId; } - public void setIrrelevantTargetContainer(IrrelevantTargetContainer irrelevantTargetContainer) { + IrrelevantTargetContainer getIrrelevantTargetContainer() { + return this.irrelevantTargetContainer; + } + + void setIrrelevantTargetContainer(IrrelevantTargetContainer irrelevantTargetContainer) { this.irrelevantTargetContainer = irrelevantTargetContainer; } + } @Node static class LonelySourceContainer { - @Id - @GeneratedValue - private Long id; @Relationship(type = "RELATIONSHIP_PROPERTY_CONTAINER") RelationshipPropertyContainer single; @@ -290,68 +301,74 @@ final class DomainClasses { @Relationship Map dynEmptySingle = new HashMap<>(); - public Long getId() { + @Id + @GeneratedValue + private Long id; + + Long getId() { return this.id; } - public RelationshipPropertyContainer getSingle() { - return this.single; - } - - public List getMultiNull() { - return this.multiNull; - } - - public List getMultiEmpty() { - return this.multiEmpty; - } - - public Map> getDynNullList() { - return this.dynNullList; - } - - public Map> getDynEmptyList() { - return this.dynEmptyList; - } - - public Map getDynNullSingle() { - return this.dynNullSingle; - } - - public Map getDynEmptySingle() { - return this.dynEmptySingle; - } - - public void setId(Long id) { + void setId(Long id) { this.id = id; } - public void setSingle(RelationshipPropertyContainer single) { + RelationshipPropertyContainer getSingle() { + return this.single; + } + + void setSingle(RelationshipPropertyContainer single) { this.single = single; } - public void setMultiNull(List multiNull) { + List getMultiNull() { + return this.multiNull; + } + + void setMultiNull(List multiNull) { this.multiNull = multiNull; } - public void setMultiEmpty(List multiEmpty) { + List getMultiEmpty() { + return this.multiEmpty; + } + + void setMultiEmpty(List multiEmpty) { this.multiEmpty = multiEmpty; } - public void setDynNullList(Map> dynNullList) { + Map> getDynNullList() { + return this.dynNullList; + } + + void setDynNullList(Map> dynNullList) { this.dynNullList = dynNullList; } - public void setDynEmptyList(Map> dynEmptyList) { + Map> getDynEmptyList() { + return this.dynEmptyList; + } + + void setDynEmptyList(Map> dynEmptyList) { this.dynEmptyList = dynEmptyList; } - public void setDynNullSingle(Map dynNullSingle) { + Map getDynNullSingle() { + return this.dynNullSingle; + } + + void setDynNullSingle(Map dynNullSingle) { this.dynNullSingle = dynNullSingle; } - public void setDynEmptySingle(Map dynEmptySingle) { + Map getDynEmptySingle() { + return this.dynEmptySingle; + } + + void setDynEmptySingle(Map dynEmptySingle) { this.dynEmptySingle = dynEmptySingle; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/properties/PropertyIT.java b/src/test/java/org/springframework/data/neo4j/integration/properties/PropertyIT.java index b380f61d5..60801f5fa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/properties/PropertyIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/properties/PropertyIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.properties; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -27,32 +25,44 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Metallica - Metallica */ @Neo4jIntegrationTest -// Not actually incompatible, but not worth the effort adding additional complexity for handling bookmarks +// Not actually incompatible, but not worth the effort adding additional complexity for +// handling bookmarks // between fixture and test @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) class PropertyIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + @Autowired + private Driver driver; + + @Autowired + private Neo4jTemplate template; + + @Autowired + private BookmarkCapture bookmarkCapture; + @BeforeAll static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { @@ -62,21 +72,14 @@ class PropertyIT { } } - @Autowired - private Driver driver; - @Autowired - private Neo4jTemplate template; - @Autowired - private BookmarkCapture bookmarkCapture; - @Test // GH-2118 void assignedIdNoVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainer {id: 'id1', knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimplePropertyContainer.class, "id1"); @@ -85,11 +88,11 @@ class PropertyIT { @Test // GH-2118 void assignedIdWithVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainerWithVersion:SimplePropertyContainer {id: 'id1', version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimplePropertyContainerWithVersion.class, "id1"); @@ -99,11 +102,13 @@ class PropertyIT { void generatedIdNoVersionShouldNotOverwriteUnknownProperties() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (m:SimpleGeneratedIDPropertyContainer {knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (m:SimpleGeneratedIDPropertyContainer {knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimpleGeneratedIDPropertyContainer.class, id); @@ -113,11 +118,13 @@ class PropertyIT { void generatedIdWithVersionShouldNotOverwriteUnknownProperties() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (m:SimpleGeneratedIDPropertyContainerWithVersion:SimpleGeneratedIDPropertyContainer {version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (m:SimpleGeneratedIDPropertyContainerWithVersion:SimpleGeneratedIDPropertyContainer {version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimpleGeneratedIDPropertyContainerWithVersion.class, id); @@ -125,18 +132,21 @@ class PropertyIT { private void updateKnownAndAssertUnknownProperty(Class type, Object id) { - Optional optionalContainer = template.findById(id, type); + Optional optionalContainer = this.template.findById(id, type); assertThat(optionalContainer).isPresent(); optionalContainer.ifPresent(m -> { m.setKnownProperty("A2"); - template.save(m); + this.template.save(m); }); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long cnt = session - .run("MATCH (m:" + type.getSimpleName() + ") WHERE " + (id instanceof Long ? "id(m) " : "m.id") - + " = $id AND m.knownProperty = 'A2' AND m.unknownProperty = 'Mr. X' RETURN count(m)", - Collections.singletonMap("id", id)).single().get(0).asLong(); + .run("MATCH (m:" + type.getSimpleName() + ") WHERE " + ((id instanceof Long) ? "id(m) " : "m.id") + + " = $id AND m.knownProperty = 'A2' AND m.unknownProperty = 'Mr. X' RETURN count(m)", + Collections.singletonMap("id", id)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -144,31 +154,36 @@ class PropertyIT { @Test // GH-2118 void multipleAssignedIdNoVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainer {id: 'a', knownProperty: 'A', unknownProperty: 'Fix'}) RETURN id(m)") - .consume(); + .consume(); session.run( "CREATE (m:SimplePropertyContainer {id: 'b', knownProperty: 'B', unknownProperty: 'Foxy'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - DomainClasses.SimplePropertyContainer optionalContainerA = template - .findById("a", DomainClasses.SimplePropertyContainer.class).get(); - DomainClasses.SimplePropertyContainer optionalContainerB = template - .findById("b", DomainClasses.SimplePropertyContainer.class).get(); + DomainClasses.SimplePropertyContainer optionalContainerA = this.template + .findById("a", DomainClasses.SimplePropertyContainer.class) + .get(); + DomainClasses.SimplePropertyContainer optionalContainerB = this.template + .findById("b", DomainClasses.SimplePropertyContainer.class) + .get(); optionalContainerA.setKnownProperty("A2"); optionalContainerB.setKnownProperty("B2"); - template.saveAll(Arrays.asList(optionalContainerA, optionalContainerB)); + this.template.saveAll(Arrays.asList(optionalContainerA, optionalContainerB)); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session - .run("MATCH (m:SimplePropertyContainer) WHERE m.id in $ids AND m.unknownProperty IS NOT NULL RETURN count(m)", - Collections.singletonMap("ids", Arrays.asList("a", "b"))).single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session.run( + "MATCH (m:SimplePropertyContainer) WHERE m.id in $ids AND m.unknownProperty IS NOT NULL RETURN count(m)", + Collections.singletonMap("ids", Arrays.asList("a", "b"))) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(2L); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -176,15 +191,17 @@ class PropertyIT { void relationshipPropertiesMustNotBeOverwritten() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (a:IrrelevantSourceContainer) - [:RELATIONSHIP_PROPERTY_CONTAINER {knownProperty: 'A', unknownProperty: 'Mr. X'}] -> (:IrrelevantTargetContainer) RETURN id(a)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (a:IrrelevantSourceContainer) - [:RELATIONSHIP_PROPERTY_CONTAINER {knownProperty: 'A', unknownProperty: 'Mr. X'}] -> (:IrrelevantTargetContainer) RETURN id(a)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - Optional optionalContainer = template - .findById(id, DomainClasses.IrrelevantSourceContainer.class); + Optional optionalContainer = this.template.findById(id, + DomainClasses.IrrelevantSourceContainer.class); assertThat(optionalContainer).hasValueSatisfying(c -> { assertThat(c.getRelationshipPropertyContainer()).isNotNull(); assertThat(c.getRelationshipPropertyContainer().getId()).isNotNull(); @@ -192,13 +209,16 @@ class PropertyIT { optionalContainer.ifPresent(c -> { c.getRelationshipPropertyContainer().setKnownProperty("A2"); - template.save(c); + this.template.save(c); }); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session - .run("MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A2' AND r.unknownProperty = 'Mr. X' RETURN count(m)", - Collections.singletonMap("id", id)).single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session.run( + "MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A2' AND r.unknownProperty = 'Mr. X' RETURN count(m)", + Collections.singletonMap("id", id)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -209,13 +229,17 @@ class PropertyIT { DomainClasses.RelationshipPropertyContainer rel = new DomainClasses.RelationshipPropertyContainer(); rel.setKnownProperty("A"); rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); - DomainClasses.IrrelevantSourceContainer s = template.save(new DomainClasses.IrrelevantSourceContainer(rel)); + DomainClasses.IrrelevantSourceContainer s = this.template + .save(new DomainClasses.IrrelevantSourceContainer(rel)); assertThat(s.getRelationshipPropertyContainer().getId()).isNotNull(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session - .run("MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A' RETURN count(m)", - Collections.singletonMap("id", s.getId())).single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session.run( + "MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A' RETURN count(m)", + Collections.singletonMap("id", s.getId())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -228,7 +252,7 @@ class PropertyIT { rel.setKnownProperty("A"); rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); source.rels.put("DYN_REL", Collections.singletonList(rel)); - source = template.save(source); + source = this.template.save(source); assertThat(source.getRels().get("DYN_REL").get(0).getId()).isNotNull(); DomainClasses.DynRelSourc2 source2 = new DomainClasses.DynRelSourc2(); @@ -236,7 +260,7 @@ class PropertyIT { rel.setKnownProperty("A"); rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); source2.rels.put("DYN_REL", rel); - source2 = template.save(source2); + source2 = this.template.save(source2); assertThat(source2.getRels().get("DYN_REL").getId()).isNotNull(); } @@ -244,12 +268,16 @@ class PropertyIT { void customConvertersForRelsMustBeTakenIntoAccount() { Date now = new Date(); - DomainClasses.WeirdSource source = new DomainClasses.WeirdSource(now, new DomainClasses.IrrelevantTargetContainer()); - template.save(source).getMyFineId(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + DomainClasses.WeirdSource source = new DomainClasses.WeirdSource(now, + new DomainClasses.IrrelevantTargetContainer()); + this.template.save(source).getMyFineId(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long cnt = session - .run("MATCH (m) - [r:ITS_COMPLICATED] -> (n) WHERE m.id = $id RETURN count(m)", - Collections.singletonMap("id", now.getTime())).single().get(0).asLong(); + .run("MATCH (m) - [r:ITS_COMPLICATED] -> (n) WHERE m.id = $id RETURN count(m)", + Collections.singletonMap("id", now.getTime())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -257,9 +285,13 @@ class PropertyIT { @Test // GH-2124 void shouldNotFailWithEmptyOrNullRelationshipProperties() { - DomainClasses.LonelySourceContainer s = template.save(new DomainClasses.LonelySourceContainer()); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run("MATCH (m) WHERE id(m) = $id RETURN count(m)", Collections.singletonMap("id", s.getId())).single().get(0).asLong(); + DomainClasses.LonelySourceContainer s = this.template.save(new DomainClasses.LonelySourceContainer()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session + .run("MATCH (m) WHERE id(m) = $id RETURN count(m)", Collections.singletonMap("id", s.getId())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -269,25 +301,30 @@ class PropertyIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/properties/ReactivePropertyIT.java b/src/test/java/org/springframework/data/neo4j/integration/properties/ReactivePropertyIT.java index 7037e9a05..9cc4b2c38 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/properties/ReactivePropertyIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/properties/ReactivePropertyIT.java @@ -15,17 +15,6 @@ */ package org.springframework.data.neo4j.integration.properties; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Tag; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -33,29 +22,49 @@ import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Metallica - Metallica */ @Neo4jIntegrationTest -// Not actually incompatible, but not worth the effort adding additional complexity for handling bookmarks +// Not actually incompatible, but not worth the effort adding additional complexity for +// handling bookmarks // between fixture and test @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) class ReactivePropertyIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + @Autowired + BookmarkCapture bookmarkCapture; + + @Autowired + private Driver driver; + + @Autowired + private ReactiveNeo4jTemplate template; + @BeforeAll static void setupData(@Autowired Driver driver) { @@ -64,21 +73,14 @@ class ReactivePropertyIT { } } - @Autowired - private Driver driver; - @Autowired - BookmarkCapture bookmarkCapture; - @Autowired - private ReactiveNeo4jTemplate template; - @Test // GH-2118 void assignedIdNoVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainer {id: 'id1', knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimplePropertyContainer.class, "id1"); @@ -87,11 +89,11 @@ class ReactivePropertyIT { @Test // GH-2118 void assignedIdWithVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainer:SimplePropertyContainerWithVersion {id: 'id1', version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimplePropertyContainerWithVersion.class, "id1"); @@ -101,11 +103,13 @@ class ReactivePropertyIT { void generatedIdNoVersionShouldNotOverwriteUnknownProperties() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (m:SimpleGeneratedIDPropertyContainer {knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (m:SimpleGeneratedIDPropertyContainer {knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimpleGeneratedIDPropertyContainer.class, id); @@ -115,11 +119,13 @@ class ReactivePropertyIT { void generatedIdWithVersionShouldNotOverwriteUnknownProperties() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (m:SimpleGeneratedIDPropertyContainer:SimpleGeneratedIDPropertyContainerWithVersion {version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (m:SimpleGeneratedIDPropertyContainer:SimpleGeneratedIDPropertyContainerWithVersion {version: 1, knownProperty: 'A', unknownProperty: 'Mr. X'}) RETURN id(m)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } updateKnownAndAssertUnknownProperty(DomainClasses.SimpleGeneratedIDPropertyContainerWithVersion.class, id); @@ -127,21 +133,23 @@ class ReactivePropertyIT { private void updateKnownAndAssertUnknownProperty(Class type, Object id) { - template.findById(id, type) - .map(m -> { - m.setKnownProperty("A2"); - return m; - }) - .flatMap(template::save) - .as(StepVerifier::create) - .expectNextMatches(c -> "A2".equals(c.getKnownProperty())) - .verifyComplete(); + this.template.findById(id, type).map(m -> { + m.setKnownProperty("A2"); + return m; + }) + .flatMap(this.template::save) + .as(StepVerifier::create) + .expectNextMatches(c -> "A2".equals(c.getKnownProperty())) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long cnt = session - .run("MATCH (m:" + type.getSimpleName() + ") WHERE " + (id instanceof Long ? "id(m) " : "m.id") - + " = $id AND m.knownProperty = 'A2' AND m.unknownProperty = 'Mr. X' RETURN count(m)", - Collections.singletonMap("id", id)).single().get(0).asLong(); + .run("MATCH (m:" + type.getSimpleName() + ") WHERE " + ((id instanceof Long) ? "id(m) " : "m.id") + + " = $id AND m.knownProperty = 'A2' AND m.unknownProperty = 'Mr. X' RETURN count(m)", + Collections.singletonMap("id", id)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -149,31 +157,34 @@ class ReactivePropertyIT { @Test // GH-2118 void multipleAssignedIdNoVersionShouldNotOverwriteUnknownProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run( "CREATE (m:SimplePropertyContainer {id: 'a', knownProperty: 'A', unknownProperty: 'Fix'}) RETURN id(m)") - .consume(); + .consume(); session.run( "CREATE (m:SimplePropertyContainer {id: 'b', knownProperty: 'B', unknownProperty: 'Foxy'}) RETURN id(m)") - .consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + .consume(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - template.findById("a", DomainClasses.SimplePropertyContainer.class) - .zipWith(template.findById("b", DomainClasses.SimplePropertyContainer.class)) - .flatMapMany(t -> { - t.getT1().setKnownProperty("A2"); - t.getT2().setKnownProperty("B2"); - return template.saveAll(t.toList()); - }) - .as(StepVerifier::create) - .expectNextCount(2) - .verifyComplete(); + this.template.findById("a", DomainClasses.SimplePropertyContainer.class) + .zipWith(this.template.findById("b", DomainClasses.SimplePropertyContainer.class)) + .flatMapMany(t -> { + t.getT1().setKnownProperty("A2"); + t.getT2().setKnownProperty("B2"); + return this.template.saveAll(t.toList()); + }) + .as(StepVerifier::create) + .expectNextCount(2) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session - .run("MATCH (m:SimplePropertyContainer) WHERE m.id in $ids AND m.unknownProperty IS NOT NULL RETURN count(m)", - Collections.singletonMap("ids", Arrays.asList("a", "b"))).single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session.run( + "MATCH (m:SimplePropertyContainer) WHERE m.id in $ids AND m.unknownProperty IS NOT NULL RETURN count(m)", + Collections.singletonMap("ids", Arrays.asList("a", "b"))) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(2L); } } @@ -182,27 +193,31 @@ class ReactivePropertyIT { void relationshipPropertiesMustNotBeOverwritten() { Long id; - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - id = session - .run("CREATE (a:IrrelevantSourceContainer) - [:RELATIONSHIP_PROPERTY_CONTAINER {knownProperty: 'A', unknownProperty: 'Mr. X'}] -> (:IrrelevantTargetContainer) RETURN id(a)") - .single().get(0).asLong(); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + id = session.run( + "CREATE (a:IrrelevantSourceContainer) - [:RELATIONSHIP_PROPERTY_CONTAINER {knownProperty: 'A', unknownProperty: 'Mr. X'}] -> (:IrrelevantTargetContainer) RETURN id(a)") + .single() + .get(0) + .asLong(); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - template.findById(id, DomainClasses.IrrelevantSourceContainer.class) - .map(c -> { - c.getRelationshipPropertyContainer().setKnownProperty("A2"); - return c; - }) - .flatMap(template::save) - .as(StepVerifier::create) - .expectNextMatches(c -> "A2".equals(c.getRelationshipPropertyContainer().getKnownProperty())) - .verifyComplete(); + this.template.findById(id, DomainClasses.IrrelevantSourceContainer.class).map(c -> { + c.getRelationshipPropertyContainer().setKnownProperty("A2"); + return c; + }) + .flatMap(this.template::save) + .as(StepVerifier::create) + .expectNextMatches(c -> "A2".equals(c.getRelationshipPropertyContainer().getKnownProperty())) + .verifyComplete(); - try (Session session = driver.session()) { - long cnt = session - .run("MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A2' AND r.unknownProperty = 'Mr. X' RETURN count(m)", - Collections.singletonMap("id", id)).single().get(0).asLong(); + try (Session session = this.driver.session()) { + long cnt = session.run( + "MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A2' AND r.unknownProperty = 'Mr. X' RETURN count(m)", + Collections.singletonMap("id", id)) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -215,15 +230,19 @@ class ReactivePropertyIT { rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); List recorded = new ArrayList<>(); - template.save(new DomainClasses.IrrelevantSourceContainer(rel)) - .as(StepVerifier::create) - .recordWith(() -> recorded) - .expectNextMatches(i -> i.getRelationshipPropertyContainer().getId() != null) - .verifyComplete(); + this.template.save(new DomainClasses.IrrelevantSourceContainer(rel)) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .expectNextMatches(i -> i.getRelationshipPropertyContainer().getId() != null) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run("MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A' RETURN count(m)", - Collections.singletonMap("id", recorded.get(0).getId())).single().get(0).asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session.run( + "MATCH (m) - [r:RELATIONSHIP_PROPERTY_CONTAINER] -> (:IrrelevantTargetContainer) WHERE id(m) = $id AND r.knownProperty = 'A' RETURN count(m)", + Collections.singletonMap("id", recorded.get(0).getId())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -236,10 +255,10 @@ class ReactivePropertyIT { rel.setKnownProperty("A"); rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); source.rels.put("DYN_REL", Collections.singletonList(rel)); - template.save(source) - .as(StepVerifier::create) - .expectNextMatches(s -> s.getRels().get("DYN_REL").get(0).getId() != null) - .verifyComplete(); + this.template.save(source) + .as(StepVerifier::create) + .expectNextMatches(s -> s.getRels().get("DYN_REL").get(0).getId() != null) + .verifyComplete(); DomainClasses.DynRelSourc2 source2 = new DomainClasses.DynRelSourc2(); rel = new DomainClasses.RelationshipPropertyContainer(); @@ -247,25 +266,28 @@ class ReactivePropertyIT { rel.setIrrelevantTargetContainer(new DomainClasses.IrrelevantTargetContainer()); source2.rels.put("DYN_REL", rel); - template.save(source2) - .as(StepVerifier::create) - .expectNextMatches(s -> s.getRels().get("DYN_REL").getId() != null) - .verifyComplete(); + this.template.save(source2) + .as(StepVerifier::create) + .expectNextMatches(s -> s.getRels().get("DYN_REL").getId() != null) + .verifyComplete(); } @Test // GH-2123 void customConvertersForRelsMustBeTakenIntoAccount() { Date now = new Date(); - template.save(new DomainClasses.WeirdSource(now, new DomainClasses.IrrelevantTargetContainer())) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + this.template.save(new DomainClasses.WeirdSource(now, new DomainClasses.IrrelevantTargetContainer())) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long cnt = session - .run("MATCH (m) - [r:ITS_COMPLICATED] -> (n) WHERE m.id = $id RETURN count(m)", - Collections.singletonMap("id", now.getTime())).single().get(0).asLong(); + .run("MATCH (m) - [r:ITS_COMPLICATED] -> (n) WHERE m.id = $id RETURN count(m)", + Collections.singletonMap("id", now.getTime())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -274,14 +296,18 @@ class ReactivePropertyIT { void shouldNotFailWithEmptyOrNullRelationshipProperties() { List recorded = new ArrayList<>(); - template.save(new DomainClasses.LonelySourceContainer()) - .map(DomainClasses.LonelySourceContainer::getId) - .as(StepVerifier::create) - .recordWith(() -> recorded) - .expectNextCount(1L) - .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run("MATCH (m) WHERE id(m) = $id RETURN count(m)", Collections.singletonMap("id", recorded.get(0))).single().get(0).asLong(); + this.template.save(new DomainClasses.LonelySourceContainer()) + .map(DomainClasses.LonelySourceContainer::getId) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .expectNextCount(1L) + .verifyComplete(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session + .run("MATCH (m) WHERE id(m) = $id RETURN count(m)", Collections.singletonMap("id", recorded.get(0))) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(1L); } } @@ -291,25 +317,30 @@ class ReactivePropertyIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/CustomReactiveBaseRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/CustomReactiveBaseRepositoryIT.java index 86f7560d1..08a9b8d1e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/CustomReactiveBaseRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/CustomReactiveBaseRepositoryIT.java @@ -15,16 +15,13 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan.Filter; @@ -37,9 +34,12 @@ import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepos import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; import org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository; import org.springframework.data.neo4j.test.DriverMocks; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * Make sure custom base repositories can be used in reactive configurations. * @@ -51,11 +51,14 @@ public class CustomReactiveBaseRepositoryIT { @Test public void customBaseRepositoryShouldBeInUse(@Autowired MyPersonRepository repository) { - StepVerifier.create(repository.findAll()).expectErrorMatches(e -> e instanceof UnsupportedOperationException - && e.getMessage().equals("This implementation does not support `findAll`")); + StepVerifier.create(repository.findAll()) + .expectErrorMatches(e -> e instanceof UnsupportedOperationException + && e.getMessage().equals("This implementation does not support `findAll`")); } - interface MyPersonRepository extends ReactiveNeo4jRepository {} + interface MyPersonRepository extends ReactiveNeo4jRepository { + + } static class MyRepositoryImpl extends SimpleReactiveNeo4jRepository { @@ -65,13 +68,14 @@ public class CustomReactiveBaseRepositoryIT { assertThat(neo4jOperations).isNotNull(); assertThat(entityInformation).isNotNull(); Assertions.assertThat(entityInformation.getEntityMetaData().getUnderlyingClass()) - .isEqualTo(PersonWithAllConstructor.class); + .isEqualTo(PersonWithAllConstructor.class); } @Override public Flux findAll() { throw new UnsupportedOperationException("This implementation does not support `findAll`"); } + } @Configuration @@ -81,6 +85,7 @@ public class CustomReactiveBaseRepositoryIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return DriverMocks.withOpenReactiveSessionAndTransaction(); } @@ -89,5 +94,7 @@ public class CustomReactiveBaseRepositoryIT { public boolean isCypher5Compatible() { return false; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingIT.java index 2c0d2e3bb..5a9c33b32 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingIT.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -28,6 +22,9 @@ import java.util.Optional; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,10 +41,13 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -67,9 +67,12 @@ class ReactiveAuditingIT extends AuditingITBase { void auditingOfCreationShouldWork(@Autowired ImmutableEntityTestRepository repository) { List newThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> repository.save(new ImmutableAuditableThing("A thing"))).as(StepVerifier::create) - .recordWith(() -> newThings).expectNextCount(1L).verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> repository.save(new ImmutableAuditableThing("A thing"))) + .as(StepVerifier::create) + .recordWith(() -> newThings) + .expectNextCount(1L) + .verifyComplete(); ImmutableAuditableThing savedThing = newThings.get(0); assertThat(savedThing.getCreatedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); @@ -84,10 +87,10 @@ class ReactiveAuditingIT extends AuditingITBase { @Test void auditingOfModificationShouldWork(@Autowired ImmutableEntityTestRepository repository) { - Mono findAndUpdateAThing = repository.findById(idOfExistingThing) - .flatMap(thing -> repository.save(thing.withName("A new name"))); + Mono findAndUpdateAThing = repository.findById(this.idOfExistingThing) + .flatMap(thing -> repository.save(thing.withName("A new name"))); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); transactionalOperator.execute(t -> findAndUpdateAThing).as(StepVerifier::create).consumeNextWith(savedThing -> { assertThat(savedThing.getCreatedAt()).isEqualTo(EXISTING_THING_CREATED_AT); @@ -99,8 +102,9 @@ class ReactiveAuditingIT extends AuditingITBase { assertThat(savedThing.getName()).isEqualTo("A new name"); }).verifyComplete(); - // Need to happen outside the reactive flow, as we use the blocking session to verify the database - verifyDatabase(idOfExistingThing, new ImmutableAuditableThing(null, EXISTING_THING_CREATED_AT, + // Need to happen outside the reactive flow, as we use the blocking session to + // verify the database + verifyDatabase(this.idOfExistingThing, new ImmutableAuditableThing(null, EXISTING_THING_CREATED_AT, EXISTING_THING_CREATED_BY, DEFAULT_CREATION_AND_MODIFICATION_DATE, "A user", "A new name")); } @@ -109,9 +113,12 @@ class ReactiveAuditingIT extends AuditingITBase { @Autowired ImmutableEntityWithGeneratedIdTestRepository repository) { List newThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); transactionalOperator.execute(t -> repository.save(new ImmutableAuditableThingWithGeneratedId("A thing"))) - .as(StepVerifier::create).recordWith(() -> newThings).expectNextCount(1L).verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> newThings) + .expectNextCount(1L) + .verifyComplete(); ImmutableAuditableThingWithGeneratedId savedThing = newThings.get(0); assertThat(savedThing.getCreatedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); @@ -128,9 +135,10 @@ class ReactiveAuditingIT extends AuditingITBase { @Autowired ImmutableEntityWithGeneratedIdTestRepository repository) { Mono findAndUpdateAThing = repository - .findById(idOfExistingThingWithGeneratedId).flatMap(thing -> repository.save(thing.withName("A new name"))); + .findById(this.idOfExistingThingWithGeneratedId) + .flatMap(thing -> repository.save(thing.withName("A new name"))); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); transactionalOperator.execute(t -> findAndUpdateAThing).as(StepVerifier::create).consumeNextWith(savedThing -> { assertThat(savedThing.getCreatedAt()).isEqualTo(EXISTING_THING_CREATED_AT); @@ -142,16 +150,21 @@ class ReactiveAuditingIT extends AuditingITBase { assertThat(savedThing.getName()).isEqualTo("A new name"); }).verifyComplete(); - // Need to happen outside the reactive flow, as we use the blocking session to verify the database - verifyDatabase(idOfExistingThingWithGeneratedId, + // Need to happen outside the reactive flow, as we use the blocking session to + // verify the database + verifyDatabase(this.idOfExistingThingWithGeneratedId, new ImmutableAuditableThingWithGeneratedId(null, EXISTING_THING_CREATED_AT, EXISTING_THING_CREATED_BY, DEFAULT_CREATION_AND_MODIFICATION_DATE, "A user", "A new name")); } - interface ImmutableEntityTestRepository extends ReactiveNeo4jRepository {} + interface ImmutableEntityTestRepository extends ReactiveNeo4jRepository { + + } interface ImmutableEntityWithGeneratedIdTestRepository - extends ReactiveNeo4jRepository {} + extends ReactiveNeo4jRepository { + + } @Configuration @EnableTransactionManagement @@ -161,35 +174,40 @@ class ReactiveAuditingIT extends AuditingITBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public ReactiveAuditorAware auditorProvider() { + ReactiveAuditorAware auditorProvider() { return () -> Mono.just("A user"); } @Bean - public DateTimeProvider fixedDateTimeProvider() { + DateTimeProvider fixedDateTimeProvider() { return () -> Optional.of(DEFAULT_CREATION_AND_MODIFICATION_DATE); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingWithoutDatesIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingWithoutDatesIT.java index cf1511720..3e87a690b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingWithoutDatesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveAuditingWithoutDatesIT.java @@ -15,18 +15,15 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,10 +38,13 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -53,7 +53,9 @@ class ReactiveAuditingWithoutDatesIT extends AuditingITBase { private final ReactiveTransactionManager transactionManager; - @Autowired ReactiveAuditingWithoutDatesIT(Driver driver, BookmarkCapture bookmarkCapture, ReactiveTransactionManager transactionManager) { + @Autowired + ReactiveAuditingWithoutDatesIT(Driver driver, BookmarkCapture bookmarkCapture, + ReactiveTransactionManager transactionManager) { super(driver, bookmarkCapture); this.transactionManager = transactionManager; @@ -63,9 +65,12 @@ class ReactiveAuditingWithoutDatesIT extends AuditingITBase { void settingOfDatesShouldBeTurnedOff(@Autowired ImmutableEntityTestRepository repository) { List newThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> repository.save(new ImmutableAuditableThing("A thing"))).as(StepVerifier::create) - .recordWith(() -> newThings).expectNextCount(1L).verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> repository.save(new ImmutableAuditableThing("A thing"))) + .as(StepVerifier::create) + .recordWith(() -> newThings) + .expectNextCount(1L) + .verifyComplete(); ImmutableAuditableThing savedThing = newThings.get(0); assertThat(savedThing.getCreatedAt()).isNull(); @@ -77,8 +82,11 @@ class ReactiveAuditingWithoutDatesIT extends AuditingITBase { verifyDatabase(savedThing.getId(), savedThing); ImmutableAuditableThing newThing = savedThing.withName("A new name"); - transactionalOperator.execute(t -> repository.save(newThing)).as(StepVerifier::create) - .recordWith(() -> newThings).expectNextCount(1L).verifyComplete(); + transactionalOperator.execute(t -> repository.save(newThing)) + .as(StepVerifier::create) + .recordWith(() -> newThings) + .expectNextCount(1L) + .verifyComplete(); savedThing = newThings.get(1); assertThat(savedThing.getCreatedAt()).isNull(); @@ -92,7 +100,9 @@ class ReactiveAuditingWithoutDatesIT extends AuditingITBase { verifyDatabase(savedThing.getId(), savedThing); } - interface ImmutableEntityTestRepository extends ReactiveNeo4jRepository {} + interface ImmutableEntityTestRepository extends ReactiveNeo4jRepository { + + } @Configuration @EnableTransactionManagement @@ -101,30 +111,35 @@ class ReactiveAuditingWithoutDatesIT extends AuditingITBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public ReactiveAuditorAware auditorProvider() { + ReactiveAuditorAware auditorProvider() { return () -> Mono.just("A user"); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCallbacksIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCallbacksIT.java index 593a43f76..d4515a6b2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCallbacksIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCallbacksIT.java @@ -15,14 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -32,6 +24,10 @@ import java.util.UUID; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,10 +42,14 @@ import org.springframework.data.neo4j.integration.shared.common.ThingWithAssigne import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + /** * @author Michael J. Simons */ @@ -74,10 +74,12 @@ class ReactiveCallbacksIT extends CallbacksITBase { Mono operationUnderTest = Mono.just(thing).flatMap(repository::save); List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> savedThings) - .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) - .verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) + .verifyComplete(); verifyDatabase(savedThings); } @@ -85,16 +87,13 @@ class ReactiveCallbacksIT extends CallbacksITBase { @Test // GH-2499 void onAfterConvertShouldBeCalledForSingleEntity(@Autowired ReactiveThingRepository repository) { - repository.findById("E1") - .as(StepVerifier::create) - .assertNext(thingWithAssignedId -> { - assertThat(thingWithAssignedId.getTheId()).isEqualTo("E1"); - assertThat(thingWithAssignedId.getRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - }) - .verifyComplete(); + repository.findById("E1").as(StepVerifier::create).assertNext(thingWithAssignedId -> { + assertThat(thingWithAssignedId.getTheId()).isEqualTo("E1"); + assertThat(thingWithAssignedId.getRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + }).verifyComplete(); } @Test @@ -109,10 +108,13 @@ class ReactiveCallbacksIT extends CallbacksITBase { Flux operationUnderTest = repository.saveAll(Arrays.asList(thing1, thing2)); List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> savedThings) - .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) - .expectNextMatches(t -> t.getName().equals("Another name (Edited)") && t.getRandomValue() == null).verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) + .expectNextMatches(t -> t.getName().equals("Another name (Edited)") && t.getRandomValue() == null) + .verifyComplete(); verifyDatabase(savedThings); } @@ -129,10 +131,13 @@ class ReactiveCallbacksIT extends CallbacksITBase { Flux operationUnderTest = repository.saveAll(Flux.just(thing1, thing2)); List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> savedThings) - .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) - .expectNextMatches(t -> t.getName().equals("Another name (Edited)") && t.getRandomValue() == null).verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)") && t.getRandomValue() == null) + .expectNextMatches(t -> t.getName().equals("Another name (Edited)") && t.getRandomValue() == null) + .verifyComplete(); verifyDatabase(savedThings); } @@ -141,21 +146,21 @@ class ReactiveCallbacksIT extends CallbacksITBase { void onAfterConvertShouldBeCalledForAllEntities(@Autowired ReactiveThingRepository repository) { repository.findAllById(Arrays.asList("E1", "E2")) - .sort(Comparator.comparing(ThingWithAssignedId::getTheId)) - .as(StepVerifier::create) - .assertNext(thingWithAssignedId -> { - assertThat(thingWithAssignedId.getTheId()).isEqualTo("E1"); - assertThat(thingWithAssignedId.getRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - }) - .assertNext(thingWithAssignedId -> { - assertThat(thingWithAssignedId.getTheId()).isEqualTo("E2"); - assertThat(thingWithAssignedId.getRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() - .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); - }) - .verifyComplete(); + .sort(Comparator.comparing(ThingWithAssignedId::getTheId)) + .as(StepVerifier::create) + .assertNext(thingWithAssignedId -> { + assertThat(thingWithAssignedId.getTheId()).isEqualTo("E1"); + assertThat(thingWithAssignedId.getRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + }) + .assertNext(thingWithAssignedId -> { + assertThat(thingWithAssignedId.getTheId()).isEqualTo("E2"); + assertThat(thingWithAssignedId.getRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + assertThat(thingWithAssignedId.getAnotherRandomValue()).isNotNull() + .satisfies(v -> assertThatNoException().isThrownBy(() -> UUID.fromString(v))); + }) + .verifyComplete(); } @Configuration @@ -166,12 +171,12 @@ class ReactiveCallbacksIT extends CallbacksITBase { @Bean ReactiveBeforeBindCallback nameChanger() { return entity -> { - ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId(), entity.getName() + " (Edited)"); + ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId(), + entity.getName() + " (Edited)"); return Mono.just(updatedThing); }; } - @Bean AfterConvertCallback randomValueAssigner() { return (entity, definition, source) -> { @@ -181,25 +186,30 @@ class ReactiveCallbacksIT extends CallbacksITBase { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCausalClusterLoadTestIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCausalClusterLoadTestIT.java index f7e44c548..8c658d640 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCausalClusterLoadTestIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCausalClusterLoadTestIT.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import org.neo4j.driver.Session; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -33,14 +28,16 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Session; import org.neo4j.junit.jupiter.causal_cluster.CausalCluster; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -51,13 +48,17 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.CausalClusterIntegrationTest; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.neo4j.test.ServerVersion; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.fail; + /** - * This tests needs a Neo4j causal cluster. We run them based on Testcontainers. It requires some resources as well as - * acceptance of the commercial license, so this test is disabled by default. + * This tests needs a Neo4j causal cluster. We run them based on Testcontainers. It + * requires some resources as well as acceptance of the commercial license, so this test + * is disabled by default. * * @author Michael J. Simons */ @@ -65,7 +66,8 @@ import org.springframework.transaction.annotation.Transactional; @Tag(Neo4jExtension.INCOMPATIBLE_WITH_CLUSTERS) class ReactiveCausalClusterLoadTestIT { - @CausalCluster private static URI neo4jUri; + @CausalCluster + private static URI neo4jUri; @RepeatedTest(20) void transactionsShouldBeSerializable(@Autowired ThingService thingService) throws InterruptedException { @@ -77,33 +79,43 @@ class ReactiveCausalClusterLoadTestIT { Callable createAndRead = () -> { List result = new ArrayList<>(); long sequenceNumber = sequence.incrementAndGet(); - thingService.newThing(sequenceNumber).then(thingService.findOneBySequenceNumber(sequenceNumber)) - .as(StepVerifier::create).recordWith((() -> result)) - .expectNextMatches(t -> t.getSequenceNumber().equals(sequenceNumber)).verifyComplete(); + thingService.newThing(sequenceNumber) + .then(thingService.findOneBySequenceNumber(sequenceNumber)) + .as(StepVerifier::create) + .recordWith((() -> result)) + .expectNextMatches(t -> t.getSequenceNumber().equals(sequenceNumber)) + .verifyComplete(); return result.get(0); }; ExecutorService executor = Executors.newCachedThreadPool(); List> executedWrites = executor - .invokeAll(IntStream.range(0, numberOfRequests).mapToObj(i -> createAndRead).collect(Collectors.toList())); + .invokeAll(IntStream.range(0, numberOfRequests).mapToObj(i -> createAndRead).collect(Collectors.toList())); try { executedWrites.forEach(request -> { try { request.get(); - } catch (InterruptedException e) {} catch (ExecutionException e) { - Assertions.fail("At least one request failed " + e.getMessage()); + } + catch (InterruptedException ex) { + } + catch (ExecutionException ex) { + fail("At least one request failed " + ex.getMessage()); } }); - } finally { + } + finally { executor.shutdown(); } } interface ThingRepository extends ReactiveNeo4jRepository { + Mono findOneBySequenceNumber(long sequenceNumber); + } static class ThingService { + private final ReactiveNeo4jClient neo4jClient; private final ThingRepository thingRepository; @@ -113,20 +125,23 @@ class ReactiveCausalClusterLoadTestIT { this.thingRepository = thingRepository; } - public Mono getMaxInstance() { - return neo4jClient.query("MATCH (t:ThingWithSequence) RETURN COALESCE(MAX(t.sequenceNumber), -1) AS maxInstance") - .fetchAs(Long.class).one(); + Mono getMaxInstance() { + return this.neo4jClient + .query("MATCH (t:ThingWithSequence) RETURN COALESCE(MAX(t.sequenceNumber), -1) AS maxInstance") + .fetchAs(Long.class) + .one(); } @Transactional - public Mono newThing(long i) { + Mono newThing(long i) { return this.thingRepository.save(new ThingWithSequence(i)); } @Transactional(readOnly = true) - public Mono findOneBySequenceNumber(long sequenceNumber) { - return thingRepository.findOneBySequenceNumber(sequenceNumber); + Mono findOneBySequenceNumber(long sequenceNumber) { + return this.thingRepository.findOneBySequenceNumber(sequenceNumber); } + } @Configuration @@ -135,6 +150,7 @@ class ReactiveCausalClusterLoadTestIT { static class TestConfig extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { Driver driver = GraphDatabase.driver(neo4jUri, AuthTokens.basic("neo4j", "secret"), @@ -144,20 +160,23 @@ class ReactiveCausalClusterLoadTestIT { } @Bean - public ThingService thingService(ReactiveNeo4jClient neo4jClient, ThingRepository thingRepository) { + ThingService thingService(ReactiveNeo4jClient neo4jClient, ThingRepository thingRepository) { return new ThingService(neo4jClient, thingRepository); } @Override public boolean isCypher5Compatible() { try (Session session = driver().session()) { - String version = session - .run("CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version") - .single() - .get("version").asString(); + String version = session.run( + "CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version") + .single() + .get("version") + .asString(); return ServerVersion.version(version).greaterThanOrEqual(ServerVersion.v4_4_0); } } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java index 94164c267..a261f8ea8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java @@ -24,6 +24,8 @@ import org.neo4j.cypherdsl.core.Property; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,7 +43,6 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import reactor.test.StepVerifier; /** * @author Niklas Krieger @@ -54,18 +55,20 @@ class ReactiveCypherdslConditionExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Node person = Cypher.node("Person").named("person"); - private final Property firstName = person.property("firstName"); - private final Property lastName = person.property("lastName"); + + private final Property firstName = this.person.property("firstName"); + + private final Property lastName = this.person.property("lastName"); @BeforeAll protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction()) { + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -75,90 +78,88 @@ class ReactiveCypherdslConditionExecutorIT { @Test void findOneShouldWork(@Autowired PersonRepository repository) { - repository.findOne(firstName.eq(Cypher.literalOf("Helge"))) - .as(StepVerifier::create) - .expectNextMatches(p -> p.getLastName().equals("Schneider")) - .verifyComplete(); + repository.findOne(this.firstName.eq(Cypher.literalOf("Helge"))) + .as(StepVerifier::create) + .expectNextMatches(p -> p.getLastName().equals("Schneider")) + .verifyComplete(); } @Test void findAllShouldWork(@Autowired PersonRepository repository) { - repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B.")))) - .map(Person::getFirstName) - .sort() - .as(StepVerifier::create) - .expectNext("Bela", "Helge") - .verifyComplete(); + repository.findAll(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B.")))) + .map(Person::getFirstName) + .sort() + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); } @Test void sortedFindAllShouldWork(@Autowired PersonRepository repository) { - repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - Sort.by("lastName").descending() - ) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "Bela") - .verifyComplete(); + repository + .findAll(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); } @Test void sortedFindAllShouldWorkWithParameter(@Autowired PersonRepository repository) { repository.findAll( - firstName.eq(Cypher.anonParameter("Helge")) - .or(lastName.eq(Cypher.parameter("someName", "B."))), // <.> - lastName.descending() // <.> - ) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "Bela") - .verifyComplete(); + this.firstName.eq(Cypher.anonParameter("Helge")) + .or(this.lastName.eq(Cypher.parameter("someName", "B."))), // <.> + this.lastName.descending() // <.> + ).map(Person::getFirstName).as(StepVerifier::create).expectNext("Helge", "Bela").verifyComplete(); } @Test void orderedFindAllShouldWork(@Autowired PersonRepository repository) { - repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), - Sort.by("lastName").descending() - ) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "Bela") - .verifyComplete(); + repository + .findAll(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); } @Test void orderedFindAllWithoutPredicateShouldWork(@Autowired PersonRepository repository) { - repository.findAll(lastName.descending()) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "B", "A", "Bela") - .verifyComplete(); + repository.findAll(this.lastName.descending()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "B", "A", "Bela") + .verifyComplete(); } @Test void countShouldWork(@Autowired PersonRepository repository) { - repository.count(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B.")))) - .as(StepVerifier::create) - .expectNext(2L) - .verifyComplete(); + repository.count(this.firstName.eq(Cypher.literalOf("Helge")).or(this.lastName.eq(Cypher.literalOf("B.")))) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); } @Test void existsShouldWork(@Autowired PersonRepository repository) { - repository.exists(firstName.eq(Cypher.literalOf("A"))) - .as(StepVerifier::create) - .expectNext(true) - .verifyComplete(); + repository.exists(this.firstName.eq(Cypher.literalOf("A"))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); } - interface PersonRepository extends ReactiveNeo4jRepository, ReactiveCypherdslConditionExecutor { + interface PersonRepository + extends ReactiveNeo4jRepository, ReactiveCypherdslConditionExecutor { + } @Configuration @@ -167,26 +168,31 @@ class ReactiveCypherdslConditionExecutorIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslStatementExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslStatementExecutorIT.java index c2eafd972..2d31c649d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslStatementExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslStatementExecutorIT.java @@ -15,15 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import org.neo4j.driver.Session; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -34,18 +25,27 @@ import org.neo4j.cypherdsl.core.Relationship; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn; import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.common.NamesOnly; import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.repository.support.ReactiveCypherdslStatementExecutor; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -58,8 +58,11 @@ class ReactiveCypherdslStatementExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; + private final Node person; + private final Property firstName; + private final Property lastName; ReactiveCypherdslStatementExecutorIT(@Autowired Driver driver) { @@ -75,13 +78,12 @@ class ReactiveCypherdslStatementExecutorIT { protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); } @@ -89,8 +91,10 @@ class ReactiveCypherdslStatementExecutorIT { static Statement whoHasFirstName(String name) { Node p = Cypher.node("Person").named("p"); - return Cypher.match(p).where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))).returning(p) - .build(); + return Cypher.match(p) + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) + .returning(p) + .build(); } static Statement whoHasFirstNameWithAddress(String name) { @@ -98,97 +102,85 @@ class ReactiveCypherdslStatementExecutorIT { Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); return Cypher.match(r) - .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ) - .build(); + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)) + .build(); } static Statement byCustomQuery() { Node p = Cypher.node("Person").named("p"); Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); - return Cypher.match(p).optionalMatch(r) - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ) - .orderBy(p.property("firstName").ascending()) - .build(); + return Cypher.match(p) + .optionalMatch(r) + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)) + .orderBy(p.property("firstName").ascending()) + .build(); } static OngoingReadingAndReturn byCustomQueryWithoutOrder() { Node p = Cypher.node("Person").named("p"); Node a = Cypher.anyNode("a"); Relationship r = p.relationshipTo(a, "LIVES_AT"); - return Cypher.match(p).optionalMatch(r) - .returning( - p.getRequiredSymbolicName(), - Cypher.collect(r), - Cypher.collect(a) - ); + return Cypher.match(p) + .optionalMatch(r) + .returning(p.getRequiredSymbolicName(), Cypher.collect(r), Cypher.collect(a)); } @Test void fineOneNoResultShouldWork(@Autowired PersonRepository repository) { - repository.findOne(whoHasFirstNameWithAddress("Farin")) - .as(StepVerifier::create) - .verifyComplete(); + repository.findOne(whoHasFirstNameWithAddress("Farin")).as(StepVerifier::create).verifyComplete(); } @Test void fineOneShouldWork(@Autowired PersonRepository repository) { repository.findOne(whoHasFirstNameWithAddress("Helge")) - .as(StepVerifier::create) - .expectNextMatches(p -> p.getFirstName().equals("Helge") && p.getLastName().equals("Schneider") && - p.getAddress().getCity().equals("Mülheim an der Ruhr")) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(p -> p.getFirstName().equals("Helge") && p.getLastName().equals("Schneider") + && p.getAddress().getCity().equals("Mülheim an der Ruhr")) + .verifyComplete(); } @Test void fineOneProjectedNoResultShouldWork(@Autowired PersonRepository repository) { - repository.findOne(whoHasFirstName("Farin"), NamesOnly.class) - .as(StepVerifier::create) - .verifyComplete(); + repository.findOne(whoHasFirstName("Farin"), NamesOnly.class).as(StepVerifier::create).verifyComplete(); } @Test void fineOneProjectedShouldWork(@Autowired PersonRepository repository) { repository.findOne(whoHasFirstName("Helge"), NamesOnly.class) - .as(StepVerifier::create) - .expectNextMatches(p -> p.getFullName().equals("Helge Schneider")) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(p -> p.getFullName().equals("Helge Schneider")) + .verifyComplete(); } @Test void findAllShouldWork(@Autowired PersonRepository repository) { repository.findAll(byCustomQuery()) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("A", "B", "Bela", "Helge") - .verifyComplete(); + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("A", "B", "Bela", "Helge") + .verifyComplete(); } @Test void findAllProjectedShouldWork(@Autowired PersonRepository repository) { repository.findAll(byCustomQuery(), NamesOnly.class) - .map(NamesOnly::getFullName) - .as(StepVerifier::create) - .expectNext("A LA", "B LB", "Bela B.", "Helge Schneider") - .verifyComplete(); + .map(NamesOnly::getFullName) + .as(StepVerifier::create) + .expectNext("A LA", "B LB", "Bela B.", "Helge Schneider") + .verifyComplete(); } - interface PersonRepository extends ReactiveNeo4jRepository, ReactiveCypherdslStatementExecutor { + interface PersonRepository + extends ReactiveNeo4jRepository, ReactiveCypherdslStatementExecutor { + } @Configuration @@ -197,26 +189,31 @@ class ReactiveCypherdslStatementExecutorIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java index f6c58683e..4aa6ba3ed 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java @@ -15,24 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.neo4j.driver.TransactionContext; -import org.junit.jupiter.api.RepeatedTest; -import org.neo4j.driver.reactive.ReactiveSession; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.integration.shared.common.CounterMetric; -import org.springframework.data.neo4j.integration.shared.common.GaugeMetric; -import org.springframework.data.neo4j.integration.shared.common.HistogramMetric; -import org.springframework.data.neo4j.integration.shared.common.Metric; -import org.springframework.data.neo4j.integration.shared.common.SummaryMetric; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; - -import reactor.adapter.JdkFlowAdapter; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -45,6 +27,7 @@ import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -55,13 +38,22 @@ import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionContext; +import org.neo4j.driver.reactive.ReactiveSession; +import reactor.adapter.JdkFlowAdapter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.CounterMetric; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.DynamicLabelsWithMultipleNodeLabels; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.DynamicLabelsWithNodeLabel; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.ExtendedBaseClass1; @@ -73,8 +65,13 @@ import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDyna import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SimpleDynamicLabelsWithVersion; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SuperNode; import org.springframework.data.neo4j.integration.shared.common.EntityWithDynamicLabelsAndIdThatNeedsToBeConverted; +import org.springframework.data.neo4j.integration.shared.common.GaugeMetric; +import org.springframework.data.neo4j.integration.shared.common.HistogramMetric; +import org.springframework.data.neo4j.integration.shared.common.Metric; +import org.springframework.data.neo4j.integration.shared.common.SummaryMetric; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -82,29 +79,127 @@ import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) @ExtendWith(Neo4jExtension.class) -public class ReactiveDynamicLabelsIT { +public final class ReactiveDynamicLabelsIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private ReactiveDynamicLabelsIT() { + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = SpringTestBase.Config.class) + @DirtiesContext + abstract static class SpringTestBase { + + @Autowired + protected Driver driver; + + @Autowired + protected TransactionalOperator transactionalOperator; + + @Autowired + protected BookmarkCapture bookmarkCapture; + + protected Long existingEntityId; + + abstract Long createTestEntity(TransactionContext t); + + @BeforeEach + void setupData() { + try (Session session = this.driver.session();) { + session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); + this.existingEntityId = session.executeWrite(this::createTestEntity); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + + @SuppressWarnings("deprecation") + protected final Flux getLabels(Long id) { + return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(Cypher.parameter("id")), id); + } + + protected final Flux getLabels(Condition idCondition, Object id) { + + Node n = Cypher.anyNode("n"); + String cypher = Renderer.getDefaultRenderer() + .render(Cypher.match(n) + .where(idCondition) + .and(n.property("moreLabels").isNull()) + .unwind(n.labels()) + .as("label") + .returning("label") + .build()); + return Flux.usingWhen(Mono.fromSupplier( + () -> this.driver.session(ReactiveSession.class, this.bookmarkCapture.createSessionConfig())), + s -> JdkFlowAdapter.flowPublisherToFlux(s.run(cypher, Collections.singletonMap("id", id))) + .flatMap(r -> JdkFlowAdapter.flowPublisherToFlux(r.records())), + rs -> JdkFlowAdapter.flowPublisherToFlux(rs.close())) + .map(r -> r.get("label").asString()); + } + + @Configuration + @EnableTransactionManagement + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); + } + + @Bean + TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { + return TransactionalOperator.create(transactionManager); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + + } @Nested class DynamicLabelsAndOrderOfClassesBeingLoaded extends SpringTestBase { @Override Long createTestEntity(TransactionContext t) { - return t.run("CREATE (m:Metric:Counter:A:B:C:D {timestamp: datetime()}) RETURN id(m)").single().get(0).asLong(); + return t.run("CREATE (m:Metric:Counter:A:B:C:D {timestamp: datetime()}) RETURN id(m)") + .single() + .get(0) + .asLong(); } @RepeatedTest(100) // GH-2619 - void ownLabelsShouldNotEndUpWithDynamicLabels(@Autowired Neo4jMappingContext mappingContext, @Autowired ReactiveNeo4jTemplate template) { + void ownLabelsShouldNotEndUpWithDynamicLabels(@Autowired Neo4jMappingContext mappingContext, + @Autowired ReactiveNeo4jTemplate template) { - List> metrics = Arrays.asList(GaugeMetric.class, SummaryMetric.class, HistogramMetric.class, CounterMetric.class); + List> metrics = Arrays.asList(GaugeMetric.class, SummaryMetric.class, + HistogramMetric.class, CounterMetric.class); Collections.shuffle(metrics); for (Class type : metrics) { assertThat(mappingContext.getPersistentEntity(type)).isNotNull(); @@ -112,15 +207,18 @@ public class ReactiveDynamicLabelsIT { Map args = new HashMap<>(); args.put("agentIdLabel", "B"); - template.findAll("MATCH (m:Metric) WHERE $agentIdLabel in labels(m) RETURN m ORDER BY m.timestamp DESC", args, Metric.class) - .as(StepVerifier::create) - .assertNext(cm -> { - assertThat(cm).isInstanceOf(CounterMetric.class); - assertThat(cm.getId()).isEqualTo(existingEntityId); - assertThat(cm.getDynamicLabels()).containsExactlyInAnyOrder("A", "B", "C", "D"); - }) - .verifyComplete(); + template + .findAll("MATCH (m:Metric) WHERE $agentIdLabel in labels(m) RETURN m ORDER BY m.timestamp DESC", args, + Metric.class) + .as(StepVerifier::create) + .assertNext(cm -> { + assertThat(cm).isInstanceOf(CounterMetric.class); + assertThat(cm.getId()).isEqualTo(this.existingEntityId); + assertThat(cm.getDynamicLabels()).containsExactlyInAnyOrder("A", "B", "C", "D"); + }) + .verifyComplete(); } + } @Nested @@ -129,28 +227,36 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar").verifyComplete(); + template.findById(this.existingEntityId, SimpleDynamicLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar") + .verifyComplete(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabels.class).flatMap(entity -> { + template.findById(this.existingEntityId, SimpleDynamicLabels.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabels").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -162,10 +268,14 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "SimpleDynamicLabels").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -179,11 +289,17 @@ public class ReactiveDynamicLabelsIT { SuperNode superNode = new SuperNode(); superNode.relatedTo = entity; - template.save(superNode).map(SuperNode::getRelatedTo).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels) - .sort().as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabels").verifyComplete(); + template.save(superNode) + .map(SuperNode::getRelatedTo) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabels") + .verifyComplete(); } + } @Nested @@ -191,30 +307,37 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:InheritedSimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabels:InheritedSimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, InheritedSimpleDynamicLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar").verifyComplete(); + template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar") + .verifyComplete(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, InheritedSimpleDynamicLabels.class).flatMap(entity -> { + template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -226,11 +349,16 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels") + .verifyComplete(); } + } @Nested @@ -239,9 +367,9 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run(""" - CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) - RETURN id(e) as existingEntityId - """).single(); + CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) + RETURN id(e) as existingEntityId + """).single(); return r.get("existingEntityId").asLong(); } @@ -252,9 +380,13 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessId").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessId") + .verifyComplete(); } @Test @@ -267,11 +399,16 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabelsWithBusinessId::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessId").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabelsWithBusinessId::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessId") + .verifyComplete(); } + } @Nested @@ -280,22 +417,26 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run( - "CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) RETURN id(e) as existingEntityId").single(); + "CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabelsWithVersion.class).flatMap(entity -> { + template.findById(this.existingEntityId, SimpleDynamicLabelsWithVersion.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) - .as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort() - .as(StepVerifier::create).expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithVersion") - .verifyComplete(); + }) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithVersion") + .verifyComplete(); } @Test @@ -307,12 +448,17 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) - .map(SimpleDynamicLabelsWithVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "SimpleDynamicLabelsWithVersion").verifyComplete(); + template.save(entity) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) + .map(SimpleDynamicLabelsWithVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithVersion") + .verifyComplete(); } + } @Nested @@ -320,7 +466,9 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId").single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -331,12 +479,15 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) - .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessIdAndVersion").verifyComplete(); + }) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) + .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessIdAndVersion") + .verifyComplete(); } @Test @@ -349,13 +500,17 @@ public class ReactiveDynamicLabelsIT { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) - .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessIdAndVersion") - .verifyComplete(); + template.save(entity) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) + .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessIdAndVersion") + .verifyComplete(); } + } @Nested @@ -364,18 +519,21 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabelsCtor.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar"); + template.findById(this.existingEntityId, SimpleDynamicLabelsCtor.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar"); } + } @Nested @@ -383,48 +541,55 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsOnClassWithSingleNodeLabel(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, DynamicLabelsWithNodeLabel.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Foo", "Foobar", "SimpleDynamicLabels") - .verifyComplete(); + template.findById(this.existingEntityId, DynamicLabelsWithNodeLabel.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Foo", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test void shouldReadDynamicLabelsOnClassWithMultipleNodeLabel(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, DynamicLabelsWithMultipleNodeLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Baz", "Foobar", "SimpleDynamicLabels") - .verifyComplete(); + template.findById(this.existingEntityId, DynamicLabelsWithMultipleNodeLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Baz", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test // GH-2296 void shouldConvertIds(@Autowired ReactiveNeo4jTemplate template) { String label = "value_1"; - Predicate expectatations = savedInstance -> - label.equals(savedInstance.getValue()) && savedInstance.getExtraLabels().contains(label); + Predicate expectatations = savedInstance -> label + .equals(savedInstance.getValue()) && savedInstance.getExtraLabels().contains(label); AtomicReference generatedUUID = new AtomicReference<>(); template.deleteAll(EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class) - .then(template.save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted(label))) - .doOnNext(s -> generatedUUID.set(s.getId())) - .as(StepVerifier::create) - .expectNextMatches(expectatations) - .verifyComplete(); + .then(template.save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted(label))) + .doOnNext(s -> generatedUUID.set(s.getId())) + .as(StepVerifier::create) + .expectNextMatches(expectatations) + .verifyComplete(); template.findById(generatedUUID.get(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class) - .as(StepVerifier::create) - .expectNextMatches(expectatations) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(expectatations) + .verifyComplete(); } + } @Nested @@ -432,91 +597,23 @@ public class ReactiveDynamicLabelsIT { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsInInheritance(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, ExtendedBaseClass1.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("D1", "D2", "D3") - .verifyComplete(); - } - } - - @ExtendWith(SpringExtension.class) - @ContextConfiguration(classes = SpringTestBase.Config.class) - @DirtiesContext - abstract static class SpringTestBase { - - @Autowired protected Driver driver; - - @Autowired protected TransactionalOperator transactionalOperator; - - @Autowired protected BookmarkCapture bookmarkCapture; - - protected Long existingEntityId; - - abstract Long createTestEntity(TransactionContext t); - - @BeforeEach - void setupData() { - try (Session session = driver.session();) { - session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); - existingEntityId = session.executeWrite(this::createTestEntity); - bookmarkCapture.seedWith(session.lastBookmarks()); - } + template.findById(this.existingEntityId, ExtendedBaseClass1.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("D1", "D2", "D3") + .verifyComplete(); } - @SuppressWarnings("deprecation") - protected final Flux getLabels(Long id) { - return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(Cypher.parameter("id")), id); - } - - protected final Flux getLabels(Condition idCondition, Object id) { - - Node n = Cypher.anyNode("n"); - String cypher = Renderer.getDefaultRenderer().render(Cypher.match(n).where(idCondition) - .and(n.property("moreLabels").isNull()).unwind(n.labels()).as("label").returning("label").build()); - return Flux - .usingWhen(Mono.fromSupplier(() -> driver.session(ReactiveSession.class, bookmarkCapture.createSessionConfig())), - s -> JdkFlowAdapter.flowPublisherToFlux(s.run(cypher, Collections.singletonMap("id", id))).flatMap(r -> JdkFlowAdapter.flowPublisherToFlux(r.records())), rs -> JdkFlowAdapter.flowPublisherToFlux(rs.close())) - .map(r -> r.get("label").asString()); - } - - @Configuration - @EnableTransactionManagement - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); - } - - @Bean - public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { - return TransactionalOperator.create(transactionManager); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicRelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicRelationshipsIT.java index dbb91a8bd..efab08e7b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicRelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicRelationshipsIT.java @@ -15,21 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; - -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.integration.shared.common.Club; -import org.springframework.data.neo4j.integration.shared.common.ClubRelationship; -import org.springframework.data.neo4j.integration.shared.common.Hobby; -import org.springframework.data.neo4j.integration.shared.common.HobbyRelationship; -import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfClub; -import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfHobby; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,12 +25,23 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.Club; +import org.springframework.data.neo4j.integration.shared.common.ClubRelationship; import org.springframework.data.neo4j.integration.shared.common.DynamicRelationshipsITBase; +import org.springframework.data.neo4j.integration.shared.common.Hobby; +import org.springframework.data.neo4j.integration.shared.common.HobbyRelationship; import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives; +import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfClub; +import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfHobby; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfPet; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelatives.TypeOfRelative; import org.springframework.data.neo4j.integration.shared.common.Pet; @@ -53,10 +49,14 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -71,7 +71,7 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { + repository.findById(this.idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); @@ -90,7 +90,7 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { + repository.findById(this.idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); @@ -99,16 +99,18 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase> hobbies = person.getHobbies(); - assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance).containsExactly("average"); + assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getPerformance) + .containsExactly("average"); assertThat(hobbies.get(TypeOfHobby.ACTIVE)).extracting(HobbyRelationship::getHobby) - .extracting(Hobby::getName).containsExactly("Biking"); + .extracting(Hobby::getName) + .containsExactly("Biking"); }).verifyComplete(); } @Test // DATAGRAPH-1449 void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).map(person -> { + repository.findById(this.idOfExistingPerson).map(person -> { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -129,26 +131,30 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase repository.findById(p.getId())) - .as(StepVerifier::create).consumeNextWith(person -> { - Map relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_DAUGHTER, TypeOfRelative.HAS_SON); - assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C2"); - assertThat(relatives.get(TypeOfRelative.HAS_SON).getFirstName()).isEqualTo("D"); + }) + .flatMap(repository::save) + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_DAUGHTER, TypeOfRelative.HAS_SON); + assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C2"); + assertThat(relatives.get(TypeOfRelative.HAS_SON).getFirstName()).isEqualTo("D"); - Map clubs = person.getClubs(); - assertThat(clubs).containsOnlyKeys(TypeOfClub.BASEBALL); - assertThat(clubs.get(TypeOfClub.BASEBALL)).extracting(ClubRelationship::getPlace).isEqualTo("Boston"); - assertThat(clubs.get(TypeOfClub.BASEBALL)).extracting(ClubRelationship::getClub) - .extracting(Club::getName).isEqualTo("Red Sox"); - }).verifyComplete(); + Map clubs = person.getClubs(); + assertThat(clubs).containsOnlyKeys(TypeOfClub.BASEBALL); + assertThat(clubs.get(TypeOfClub.BASEBALL)).extracting(ClubRelationship::getPlace).isEqualTo("Boston"); + assertThat(clubs.get(TypeOfClub.BASEBALL)).extracting(ClubRelationship::getClub) + .extracting(Club::getName) + .isEqualTo("Red Sox"); + }) + .verifyComplete(); } @Test // GH-216 // DATAGRAPH-1449 void shouldUpdateDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).map(person -> { + repository.findById(this.idOfExistingPerson).map(person -> { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -169,22 +175,26 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase repository.findById(p.getId())) - .as(StepVerifier::create).consumeNextWith(person -> { - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.FISH); - assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", - "Delilah"); - assertThat(pets.get(TypeOfPet.FISH)).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + }) + .flatMap(repository::save) + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.FISH); + assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName) + .containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get(TypeOfPet.FISH)).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); - Map> hobbies = person.getHobbies(); - assertThat(hobbies).containsOnlyKeys(TypeOfHobby.WATCHING); - assertThat(hobbies.get(TypeOfHobby.WATCHING)).extracting(HobbyRelationship::getPerformance) + Map> hobbies = person.getHobbies(); + assertThat(hobbies).containsOnlyKeys(TypeOfHobby.WATCHING); + assertThat(hobbies.get(TypeOfHobby.WATCHING)).extracting(HobbyRelationship::getPerformance) .containsExactly("average"); - assertThat(hobbies.get(TypeOfHobby.WATCHING)).extracting(HobbyRelationship::getHobby) - .extracting(Hobby::getName).containsExactly("Football"); - }).verifyComplete(); + assertThat(hobbies.get(TypeOfHobby.WATCHING)).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Football"); + }) + .verifyComplete(); } @Test // DATAGRAPH-1447 @@ -213,21 +223,30 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase recorded = new ArrayList<>(); repository.save(newPerson) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create).recordWith(() -> recorded) - .consumeNextWith(personWithRelatives -> { - Map relatives = personWithRelatives.getRelatives(); - assertThat(relatives).containsOnlyKeys(TypeOfRelative.RELATIVE_1, TypeOfRelative.RELATIVE_2); - }).verifyComplete(); + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(personWithRelatives -> { + Map relatives = personWithRelatives.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.RELATIVE_1, TypeOfRelative.RELATIVE_2); + }) + .verifyComplete(); - try (Transaction transaction = driver.session(bookmarkCapture.createSessionConfig()).beginTransaction()) { + try (Transaction transaction = this.driver.session(this.bookmarkCapture.createSessionConfig()) + .beginTransaction()) { long numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -246,8 +265,8 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase fish = pets.computeIfAbsent(TypeOfPet.FISH, s -> new ArrayList<>()); fish.add(new Pet("Nemo")); - List hobbyRelationships = hobbies - .computeIfAbsent(TypeOfHobby.ACTIVE, s -> new ArrayList<>()); + List hobbyRelationships = hobbies.computeIfAbsent(TypeOfHobby.ACTIVE, + s -> new ArrayList<>()); HobbyRelationship hobbyRelationship = new HobbyRelationship("ok"); Hobby hobby1 = new Hobby(); hobby1.setName("Football"); @@ -263,28 +282,37 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase recorded = new ArrayList<>(); repository.save(newPerson) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create) - .recordWith(() -> recorded).consumeNextWith(person -> { - Map> writtenPets = person.getPets(); - assertThat(writtenPets).containsOnlyKeys(TypeOfPet.MONSTERS, TypeOfPet.FISH); - }).verifyComplete(); + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(person -> { + Map> writtenPets = person.getPets(); + assertThat(writtenPets).containsOnlyKeys(TypeOfPet.MONSTERS, TypeOfPet.FISH); + }) + .verifyComplete(); - try (Transaction transaction = driver.session(bookmarkCapture.createSessionConfig()).beginTransaction()) { + try (Transaction transaction = this.driver.session(this.bookmarkCapture.createSessionConfig()) + .beginTransaction()) { long numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(3L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } - interface PersonWithRelativesRepository extends ReactiveNeo4jRepository {} + interface PersonWithRelativesRepository extends ReactiveNeo4jRepository { + + } @Configuration @EnableTransactionManagement @@ -292,25 +320,30 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase driver.session(ReactiveSession.class), - session -> Flux.from(session.run("MATCH (n:SimplePerson) DETACH DELETE n")) - .flatMap(ReactiveResult::records), - ReactiveSession::close) - .then().as(StepVerifier::create).verifyComplete(); + session -> Flux.from(session.run("MATCH (n:SimplePerson) DETACH DELETE n")) + .flatMap(ReactiveResult::records), + ReactiveSession::close) + .then() + .as(StepVerifier::create) + .verifyComplete(); } @Test void exceptionsFromClientShouldBeTranslated(@Autowired ReactiveNeo4jClient neo4jClient) { - neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})").run().then().as(StepVerifier::create) - .verifyComplete(); + neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})") + .run() + .then() + .as(StepVerifier::create) + .verifyComplete(); - neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})").run().as(StepVerifier::create) - .verifyErrorMatches(aTranslatedException); + neo4jClient.query("CREATE (:SimplePerson {name: 'Tom'})") + .run() + .as(StepVerifier::create) + .verifyErrorMatches(this.aTranslatedException); } @Test void exceptionsFromRepositoriesShouldBeTranslated(@Autowired SimplePersonRepository repository) { repository.save(new SimplePerson("Tom")).then().as(StepVerifier::create).verifyComplete(); - repository.save(new SimplePerson("Tom")).as(StepVerifier::create).verifyErrorMatches(aTranslatedException); + repository.save(new SimplePerson("Tom")).as(StepVerifier::create).verifyErrorMatches(this.aTranslatedException); } @Test void exceptionsOnRepositoryBeansShouldBeTranslated(@Autowired CustomDAO customDAO) { customDAO.createPerson().then().as(StepVerifier::create).verifyComplete(); - customDAO.createPerson().as(StepVerifier::create).verifyErrorMatches(aTranslatedException); + customDAO.createPerson().as(StepVerifier::create).verifyErrorMatches(this.aTranslatedException); + } + + interface SimplePersonRepository extends ReactiveNeo4jRepository { + } @Configuration @@ -130,41 +143,40 @@ class ReactiveExceptionTranslationTest { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public CustomDAO customDAO(ReactiveNeo4jClient neo4jClient) { + CustomDAO customDAO(ReactiveNeo4jClient neo4jClient) { return new CustomDAO(neo4jClient); } - // If someone wants to use the plain driver or the delegating mechanism of the client, than they must provide a + // If someone wants to use the plain driver or the delegating mechanism of the + // client, then they must provide a // couple of more beans. @Bean - public Neo4jPersistenceExceptionTranslator neo4jPersistenceExceptionTranslator() { + Neo4jPersistenceExceptionTranslator neo4jPersistenceExceptionTranslator() { return new Neo4jPersistenceExceptionTranslator(); } @Bean - public ReactivePersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() { + ReactivePersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() { return new ReactivePersistenceExceptionTranslationPostProcessor(); } @Bean - public Migrations migrations(@Autowired Driver driver) { - return new Migrations( - MigrationsConfig.builder().withLocationsToScan("classpath:/data/migrations") - .build(), driver); + Migrations migrations(@Autowired Driver driver) { + return new Migrations(MigrationsConfig.builder().withLocationsToScan("classpath:/data/migrations").build(), + driver); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } - } - interface SimplePersonRepository extends ReactiveNeo4jRepository { } @Repository @@ -176,11 +188,14 @@ class ReactiveExceptionTranslationTest { this.neo4jClient = neo4jClient; } - public Mono createPerson() { - return neo4jClient.delegateTo( - rxQueryRunner -> - Flux.from(rxQueryRunner.run("CREATE (:SimplePerson {name: 'Tom'})")) - .flatMap(ReactiveResult::consume).single()).run(); + Mono createPerson() { + return this.neo4jClient + .delegateTo(rxQueryRunner -> Flux.from(rxQueryRunner.run("CREATE (:SimplePerson {name: 'Tom'})")) + .flatMap(ReactiveResult::consume) + .single()) + .run(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveIdGeneratorsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveIdGeneratorsIT.java index b5a62172f..b4ead5298 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveIdGeneratorsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveIdGeneratorsIT.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -31,6 +25,9 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,11 +41,14 @@ import org.springframework.data.neo4j.integration.shared.common.ThingWithIdGener import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -58,7 +58,8 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { private final ReactiveTransactionManager transactionManager; @Autowired - ReactiveIdGeneratorsIT(Driver driver, BookmarkCapture bookmarkCapture, ReactiveTransactionManager transactionManager) { + ReactiveIdGeneratorsIT(Driver driver, BookmarkCapture bookmarkCapture, + ReactiveTransactionManager transactionManager) { super(driver, bookmarkCapture); this.transactionManager = transactionManager; @@ -68,13 +69,16 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { void idGenerationWithNewEntityShouldWork(@Autowired ThingWithGeneratedIdRepository repository) { List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); transactionalOperator.execute(t -> repository.save(new ThingWithGeneratedId("WrapperService"))) - .as(StepVerifier::create).recordWith(() -> savedThings).consumeNextWith(savedThing -> { + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { - assertThat(savedThing.getName()).isEqualTo("WrapperService"); - assertThat(savedThing.getTheId()).isNotBlank().matches("thingWithGeneratedId-\\d+"); - }).verifyComplete(); + assertThat(savedThing.getName()).isEqualTo("WrapperService"); + assertThat(savedThing.getTheId()).isNotBlank().matches("thingWithGeneratedId-\\d+"); + }) + .verifyComplete(); verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); } @@ -83,13 +87,16 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { void idGenerationByBeansShouldWorkWork(@Autowired ThingWithIdGeneratedByBeanRepository repository) { List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); transactionalOperator.execute(t -> repository.save(new ThingWithIdGeneratedByBean("WrapperService"))) - .as(StepVerifier::create).recordWith(() -> savedThings).consumeNextWith(savedThing -> { + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { - assertThat(savedThing.getName()).isEqualTo("WrapperService"); - assertThat(savedThing.getTheId()).isEqualTo("ReactiveID."); - }).verifyComplete(); + assertThat(savedThing.getName()).isEqualTo("WrapperService"); + assertThat(savedThing.getTheId()).isEqualTo("ReactiveID."); + }) + .verifyComplete(); verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); } @@ -97,18 +104,23 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { @Test void idGenerationWithNewEntitiesShouldWork(@Autowired ThingWithGeneratedIdRepository repository) { - List things = IntStream.rangeClosed(1, 10).mapToObj(i -> new ThingWithGeneratedId("name" + i)) - .collect(Collectors.toList()); + List things = IntStream.rangeClosed(1, 10) + .mapToObj(i -> new ThingWithGeneratedId("name" + i)) + .collect(Collectors.toList()); Set generatedIds = new HashSet<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> repository.saveAll(things)).map(ThingWithGeneratedId::getTheId) - .as(StepVerifier::create).recordWith(() -> generatedIds).expectNextCount(things.size()) - .expectRecordedMatches(recorded -> { - assertThat(recorded).hasSize(things.size()) - .allMatch(generatedId -> generatedId.matches("thingWithGeneratedId-\\d+")); - return true; - }).verifyComplete(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> repository.saveAll(things)) + .map(ThingWithGeneratedId::getTheId) + .as(StepVerifier::create) + .recordWith(() -> generatedIds) + .expectNextCount(things.size()) + .expectRecordedMatches(recorded -> { + assertThat(recorded).hasSize(things.size()) + .allMatch(generatedId -> generatedId.matches("thingWithGeneratedId-\\d+")); + return true; + }) + .verifyComplete(); } @Test @@ -120,20 +132,27 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { }); List savedThings = new ArrayList<>(); - TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); - transactionalOperator.execute(t -> findAndUpdateAThing).as(StepVerifier::create).recordWith(() -> savedThings) - .consumeNextWith(savedThing -> { + TransactionalOperator transactionalOperator = TransactionalOperator.create(this.transactionManager); + transactionalOperator.execute(t -> findAndUpdateAThing) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { - assertThat(savedThing.getName()).isEqualTo("changed"); - assertThat(savedThing.getTheId()).isEqualTo(ID_OF_EXISTING_THING); - }).verifyComplete(); + assertThat(savedThing.getName()).isEqualTo("changed"); + assertThat(savedThing.getTheId()).isEqualTo(ID_OF_EXISTING_THING); + }) + .verifyComplete(); verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); } - interface ThingWithGeneratedIdRepository extends ReactiveCrudRepository {} + interface ThingWithGeneratedIdRepository extends ReactiveCrudRepository { - interface ThingWithIdGeneratedByBeanRepository extends ReactiveCrudRepository {} + } + + interface ThingWithIdGeneratedByBeanRepository extends ReactiveCrudRepository { + + } @Configuration @EnableTransactionManagement @@ -141,30 +160,35 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public IdGenerator aFancyIdGenerator() { + IdGenerator aFancyIdGenerator() { return (label, entity) -> "ReactiveID."; } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableAssignedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableAssignedIdsIT.java index 44b50804e..6d9503014 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableAssignedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableAssignedIdsIT.java @@ -15,12 +15,24 @@ */ package org.springframework.data.neo4j.integration.reactive; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,17 +53,6 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -63,7 +64,9 @@ import static org.assertj.core.api.Assertions.assertThat; public class ReactiveImmutableAssignedIdsIT { public static final String SOME_VALUE_VALUE = "testValue"; + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private final Driver driver; public ReactiveImmutableAssignedIdsIT(@Autowired Driver driver) { @@ -72,7 +75,7 @@ public class ReactiveImmutableAssignedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -86,16 +89,14 @@ public class ReactiveImmutableAssignedIdsIT { ImmutablePersonWithAssignedId fallback2 = ImmutablePersonWithAssignedId.fallback(fallback1); ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.fallback(fallback2); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.fallback).isNotNull(); - assertThat(savedPerson.fallback.fallback).isNotNull(); - assertThat(savedPerson.someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.fallback.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.fallback).isNotNull(); + assertThat(savedPerson.fallback.fallback).isNotNull(); + assertThat(savedPerson.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.fallback.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2141 @@ -106,16 +107,14 @@ public class ReactiveImmutableAssignedIdsIT { ImmutablePersonWithAssignedId fallback2 = ImmutablePersonWithAssignedId.fallback(fallback1); ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.fallback(fallback2); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.fallback).isNotNull(); - assertThat(savedPerson.fallback.fallback).isNotNull(); - assertThat(savedPerson.someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.fallback.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.fallback).isNotNull(); + assertThat(savedPerson.fallback.fallback).isNotNull(); + assertThat(savedPerson.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.fallback.fallback.someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -123,14 +122,13 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId onboarder = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .wasOnboardedBy(Collections.singletonList(onboarder)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - assertThat(savedPerson.wasOnboardedBy.get(0).someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy.get(0).someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -138,14 +136,13 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId knowingPerson = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .knownBy(Collections.singleton(knowingPerson)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - assertThat(savedPerson.knownBy.iterator().next().someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + assertThat(savedPerson.knownBy.iterator().next().someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -153,15 +150,14 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId rater = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .ratedBy(Collections.singletonMap("Good", rater)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedBy.values().iterator().next().someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + assertThat(savedPerson.ratedBy.values().iterator().next().someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -175,15 +171,13 @@ public class ReactiveImmutableAssignedIdsIT { raterMap.put("Bad", rater2); ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.ratedBy(raterMap); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedBy.keySet()).containsExactlyInAnyOrder("Good", "Bad"); - assertThat(savedPerson.ratedBy.get("Good").id).isNotNull(); - assertThat(savedPerson.ratedBy.get("Good").someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.ratedBy.get("Bad").id).isNotNull(); - assertThat(savedPerson.ratedBy.get("Bad").someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedBy.keySet()).containsExactlyInAnyOrder("Good", "Bad"); + assertThat(savedPerson.ratedBy.get("Good").id).isNotNull(); + assertThat(savedPerson.ratedBy.get("Good").someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.ratedBy.get("Bad").id).isNotNull(); + assertThat(savedPerson.ratedBy.get("Bad").someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -191,13 +185,12 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutableSecondPersonWithAssignedId rater = new ImmutableSecondPersonWithAssignedId(); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -205,16 +198,15 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipProperties(properties); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipProperties.name).isNotNull(); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - assertThat(savedPerson.relationshipProperties.target.someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipProperties.name).isNotNull(); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + assertThat(savedPerson.relationshipProperties.target.someValue).isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -222,16 +214,17 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.someValue) + .isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } @Test // GH-2148 @@ -239,121 +232,118 @@ public class ReactiveImmutableAssignedIdsIT { @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutablePersonWithAssignedId somebody = new ImmutablePersonWithAssignedId(); - ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithAssignedIdRelationshipProperties properties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue).isEqualTo(SOME_VALUE_VALUE); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue) + .isEqualTo(SOME_VALUE_VALUE); + }).verifyComplete(); } - @Test // GH-2148 void saveRelationshipWithAssignedIdsContainsObjectWithIdSetForRelationshipPropertiesDynamicCollection( @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { ImmutableSecondPersonWithAssignedId somebody = new ImmutableSecondPersonWithAssignedId(); - ImmutableSecondPersonWithAssignedIdRelationshipProperties properties = new ImmutableSecondPersonWithAssignedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithAssignedIdRelationshipProperties properties = new ImmutableSecondPersonWithAssignedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithAssignedId person = ImmutablePersonWithAssignedId.relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } @Test // GH-2148 void saveRelationshipWithAssignedIdsContainsAllRelationshipTypes( @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository) { - ImmutablePersonWithAssignedId fallback = - new ImmutablePersonWithAssignedId(); + ImmutablePersonWithAssignedId fallback = new ImmutablePersonWithAssignedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithAssignedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithAssignedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithAssignedId()); + Set knownBy = Collections.singleton(new ImmutablePersonWithAssignedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithAssignedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithAssignedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithAssignedId())); + Map> ratedByCollection = Collections.singletonMap("Na", + Collections.singletonList(new ImmutableSecondPersonWithAssignedId())); - ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithAssignedId()); + ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithAssignedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithAssignedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithAssignedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithAssignedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithAssignedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithAssignedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithAssignedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithAssignedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithAssignedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithAssignedIdRelationshipProperties(null, + "rel4", new ImmutableSecondPersonWithAssignedId()))); - ImmutablePersonWithAssignedId person = new ImmutablePersonWithAssignedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + ImmutablePersonWithAssignedId person = new ImmutablePersonWithAssignedId(null, wasOnboardedBy, knownBy, ratedBy, + ratedByCollection, fallback, relationshipProperties, relationshipPropertiesCollection, + relationshipPropertiesDynamic, relationshipPropertiesDynamicCollection); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - assertThat(savedPerson.fallback.id).isNotNull(); + assertThat(savedPerson.fallback.id).isNotNull(); - assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - assertThat(savedPerson.relationshipProperties.target.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + assertThat(savedPerson.relationshipProperties.target.someValue).isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.someValue) + .isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue).isEqualTo(SOME_VALUE_VALUE); + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.someValue) + .isEqualTo(SOME_VALUE_VALUE); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Nope"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } - @Test // GH-2235 - void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode(@Autowired ReactiveImmutablePersonWithAssignedIdRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + void saveWithGeneratedIdsWithMultipleRelationshipsToOneNode( + @Autowired ReactiveImmutablePersonWithAssignedIdRepository repository, + @Autowired BookmarkCapture bookmarkCapture) { ImmutablePersonWithAssignedId person1 = new ImmutablePersonWithAssignedId(); ImmutablePersonWithAssignedId person2 = ImmutablePersonWithAssignedId.fallback(person1); List onboardedBy = new ArrayList<>(); @@ -361,31 +351,32 @@ public class ReactiveImmutableAssignedIdsIT { onboardedBy.add(person2); ImmutablePersonWithAssignedId person3 = ImmutablePersonWithAssignedId.wasOnboardedBy(onboardedBy); - StepVerifier.create(repository.save(person3)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); + StepVerifier.create(repository.save(person3)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithAssignedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null) - .findFirst().get(); + ImmutablePersonWithAssignedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); - assertThat(savedPerson2.fallback.id).isNotNull(); - }) - .verifyComplete(); + assertThat(savedPerson2.fallback.id).isNotNull(); + }).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithAssignedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithAssignedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithAssignedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + List result = session + .run("MATCH (person3:ImmutablePersonWithAssignedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithAssignedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithAssignedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } - interface ReactiveImmutablePersonWithAssignedIdRepository extends ReactiveNeo4jRepository { + interface ReactiveImmutablePersonWithAssignedIdRepository + extends ReactiveNeo4jRepository { + } @Configuration @@ -393,6 +384,7 @@ public class ReactiveImmutableAssignedIdsIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -411,7 +403,9 @@ public class ReactiveImmutableAssignedIdsIT { } @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -421,20 +415,24 @@ public class ReactiveImmutableAssignedIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableExternallyGeneratedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableExternallyGeneratedIdsIT.java index 62bf9cbe8..8657e203b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableExternallyGeneratedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableExternallyGeneratedIdsIT.java @@ -15,11 +15,23 @@ */ package org.springframework.data.neo4j.integration.reactive; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,17 +51,6 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -60,6 +61,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class ReactiveImmutableExternallyGeneratedIdsIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private final Driver driver; public ReactiveImmutableExternallyGeneratedIdsIT(@Autowired Driver driver) { @@ -68,7 +70,7 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -79,16 +81,15 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId fallback1 = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId.fallback(fallback1); + ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId + .fallback(fallback1); ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.fallback(fallback2); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.fallback).isNotNull(); - assertThat(savedPerson.fallback.fallback).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.fallback).isNotNull(); + assertThat(savedPerson.fallback.fallback).isNotNull(); + }).verifyComplete(); } @Test // GH-41 @@ -96,16 +97,15 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId fallback1 = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId.fallback(fallback1); + ImmutablePersonWithExternallyGeneratedId fallback2 = ImmutablePersonWithExternallyGeneratedId + .fallback(fallback1); ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.fallback(fallback2); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.fallback).isNotNull(); - assertThat(savedPerson.fallback.fallback).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.fallback).isNotNull(); + assertThat(savedPerson.fallback.fallback).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -113,13 +113,12 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId onboarder = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .wasOnboardedBy(Collections.singletonList(onboarder)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -127,13 +126,12 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId knowingPerson = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .knownBy(Collections.singleton(knowingPerson)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -141,14 +139,13 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId rater = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .ratedBy(Collections.singletonMap("Good", rater)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -162,13 +159,11 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { raterMap.put("Bad", rater2); ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.ratedBy(raterMap); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedBy.keySet()).containsExactlyInAnyOrder("Good", "Bad"); - assertThat(savedPerson.ratedBy.get("Good").id).isNotNull(); - assertThat(savedPerson.ratedBy.get("Bad").id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedBy.keySet()).containsExactlyInAnyOrder("Good", "Bad"); + assertThat(savedPerson.ratedBy.get("Good").id).isNotNull(); + assertThat(savedPerson.ratedBy.get("Bad").id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -176,13 +171,12 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutableSecondPersonWithExternallyGeneratedId rater = new ImmutableSecondPersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -190,15 +184,15 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipProperties(properties); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipProperties(properties); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipProperties.name).isNotNull(); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipProperties.name).isNotNull(); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -206,15 +200,15 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -222,111 +216,108 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutablePersonWithExternallyGeneratedId somebody = new ImmutablePersonWithExternallyGeneratedId(); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + }).verifyComplete(); } - @Test // GH-2148 void saveRelationshipWithExternallyGeneratedIdsContainsObjectWithIdSetForRelationshipPropertiesDynamicCollection( @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { ImmutableSecondPersonWithExternallyGeneratedId somebody = new ImmutableSecondPersonWithExternallyGeneratedId(); - ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithExternallyGeneratedId person = ImmutablePersonWithExternallyGeneratedId + .relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } @Test // GH-2148 void saveRelationshipWithExternallyGeneratedIdsContainsAllRelationshipTypes( @Autowired ReactiveImmutablePersonWithExternalIdRepository repository) { - ImmutablePersonWithExternallyGeneratedId fallback = - new ImmutablePersonWithExternallyGeneratedId(); + ImmutablePersonWithExternallyGeneratedId fallback = new ImmutablePersonWithExternallyGeneratedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithExternallyGeneratedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithExternallyGeneratedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithExternallyGeneratedId()); + Set knownBy = Collections + .singleton(new ImmutablePersonWithExternallyGeneratedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithExternallyGeneratedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithExternallyGeneratedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedId())); + Map> ratedByCollection = Collections + .singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedId())); - ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithExternallyGeneratedId()); + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithExternallyGeneratedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithExternallyGeneratedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithExternallyGeneratedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithExternallyGeneratedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithExternallyGeneratedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithExternallyGeneratedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties( + null, "rel4", new ImmutableSecondPersonWithExternallyGeneratedId()))); ImmutablePersonWithExternallyGeneratedId person = new ImmutablePersonWithExternallyGeneratedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + wasOnboardedBy, knownBy, ratedBy, ratedByCollection, fallback, relationshipProperties, + relationshipPropertiesCollection, relationshipPropertiesDynamic, + relationshipPropertiesDynamicCollection); - StepVerifier.create(repository.saveAll(Collections.singleton(person))) - .assertNext(savedPerson -> { + StepVerifier.create(repository.saveAll(Collections.singleton(person))).assertNext(savedPerson -> { - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - assertThat(savedPerson.fallback.id).isNotNull(); + assertThat(savedPerson.fallback.id).isNotNull(); - assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Nope"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } @Test // GH-2235 @@ -339,33 +330,35 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { List onboardedBy = new ArrayList<>(); onboardedBy.add(person1); onboardedBy.add(person2); - ImmutablePersonWithExternallyGeneratedId person3 = ImmutablePersonWithExternallyGeneratedId.wasOnboardedBy(onboardedBy); + ImmutablePersonWithExternallyGeneratedId person3 = ImmutablePersonWithExternallyGeneratedId + .wasOnboardedBy(onboardedBy); - StepVerifier.create(repository.save(person3)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); + StepVerifier.create(repository.save(person3)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithExternallyGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null) - .findFirst().get(); + ImmutablePersonWithExternallyGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); - assertThat(savedPerson2.fallback.id).isNotNull(); - }) - .verifyComplete(); + assertThat(savedPerson2.fallback.id).isNotNull(); + }).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithExternallyGeneratedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithExternallyGeneratedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithExternallyGeneratedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + List result = session + .run("MATCH (person3:ImmutablePersonWithExternallyGeneratedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithExternallyGeneratedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithExternallyGeneratedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } - interface ReactiveImmutablePersonWithExternalIdRepository extends ReactiveNeo4jRepository { + interface ReactiveImmutablePersonWithExternalIdRepository + extends ReactiveNeo4jRepository { + } @Configuration @@ -373,6 +366,7 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -383,7 +377,9 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { } @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -393,20 +389,24 @@ public class ReactiveImmutableExternallyGeneratedIdsIT { } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableGeneratedIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableGeneratedIdsIT.java index b56c90243..760d42648 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableGeneratedIdsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveImmutableGeneratedIdsIT.java @@ -15,19 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.BeforeEach; -import org.neo4j.driver.Record; -import org.neo4j.driver.Session; -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -35,13 +22,21 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.common.ImmutablePersonWithGeneratedId; import org.springframework.data.neo4j.integration.shared.common.ImmutablePersonWithGeneratedIdRelationshipProperties; import org.springframework.data.neo4j.integration.shared.common.ImmutableSecondPersonWithGeneratedId; @@ -50,10 +45,15 @@ import org.springframework.data.neo4j.integration.shared.common.MutableChild; import org.springframework.data.neo4j.integration.shared.common.MutableParent; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier */ @@ -62,6 +62,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; public class ReactiveImmutableGeneratedIdsIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private final Driver driver; public ReactiveImmutableGeneratedIdsIT(@Autowired Driver driver) { @@ -70,7 +71,7 @@ public class ReactiveImmutableGeneratedIdsIT { @BeforeEach void cleanUp(@Autowired BookmarkCapture bookmarkCapture) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -84,13 +85,11 @@ public class ReactiveImmutableGeneratedIdsIT { ImmutablePersonWithGeneratedId fallback2 = ImmutablePersonWithGeneratedId.fallback(fallback1); ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.fallback(fallback2); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.fallback).isNotNull(); - assertThat(savedPerson.fallback.fallback).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.fallback).isNotNull(); + assertThat(savedPerson.fallback.fallback).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -98,14 +97,13 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId onboarder = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.wasOnboardedBy(Collections.singletonList(onboarder)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .wasOnboardedBy(Collections.singletonList(onboarder)); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -113,14 +111,13 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId knowingPerson = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.knownBy(Collections.singleton(knowingPerson)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .knownBy(Collections.singleton(knowingPerson)); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -128,15 +125,14 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId rater = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.ratedBy(Collections.singletonMap("Good", rater)); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .ratedBy(Collections.singletonMap("Good", rater)); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -144,14 +140,13 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutableSecondPersonWithGeneratedId rater = new ImmutableSecondPersonWithGeneratedId(); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .ratedByCollection(Collections.singletonMap("Good", Collections.singletonList(rater))); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -159,16 +154,15 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipProperties(properties); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.relationshipProperties.name).isNotNull(); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.relationshipProperties.name).isNotNull(); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -176,16 +170,16 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesCollection(Collections.singletonList(properties)); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .relationshipPropertiesCollection(Collections.singletonList(properties)); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -193,17 +187,17 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutablePersonWithGeneratedId somebody = new ImmutablePersonWithGeneratedId(); - ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); + ImmutablePersonWithGeneratedIdRelationshipProperties properties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId + .relationshipPropertiesDynamic(Collections.singletonMap("Good", properties)); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + }).verifyComplete(); } @@ -212,98 +206,90 @@ public class ReactiveImmutableGeneratedIdsIT { @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { ImmutableSecondPersonWithGeneratedId somebody = new ImmutableSecondPersonWithGeneratedId(); - ImmutableSecondPersonWithGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithGeneratedIdRelationshipProperties(null, "blubb", somebody); - ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamicCollection(Collections.singletonMap("Good", Collections.singletonList(properties))); + ImmutableSecondPersonWithGeneratedIdRelationshipProperties properties = new ImmutableSecondPersonWithGeneratedIdRelationshipProperties( + null, "blubb", somebody); + ImmutablePersonWithGeneratedId person = ImmutablePersonWithGeneratedId.relationshipPropertiesDynamicCollection( + Collections.singletonMap("Good", Collections.singletonList(properties))); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { + assertThat(person.id).isNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Good"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } @Test // GH-2148 void saveRelationshipWithGeneratedIdsContainsAllRelationshipTypes( @Autowired ReactiveImmutablePersonWithGeneratedIdRepository repository) { - ImmutablePersonWithGeneratedId fallback = - new ImmutablePersonWithGeneratedId(); + ImmutablePersonWithGeneratedId fallback = new ImmutablePersonWithGeneratedId(); - List wasOnboardedBy = - Collections.singletonList(new ImmutablePersonWithGeneratedId()); + List wasOnboardedBy = Collections + .singletonList(new ImmutablePersonWithGeneratedId()); - Set knownBy = - Collections.singleton(new ImmutablePersonWithGeneratedId()); + Set knownBy = Collections.singleton(new ImmutablePersonWithGeneratedId()); - Map ratedBy = - Collections.singletonMap("Good", new ImmutablePersonWithGeneratedId()); + Map ratedBy = Collections.singletonMap("Good", + new ImmutablePersonWithGeneratedId()); - Map> ratedByCollection = - Collections.singletonMap("Na", Collections.singletonList(new ImmutableSecondPersonWithGeneratedId())); + Map> ratedByCollection = Collections.singletonMap("Na", + Collections.singletonList(new ImmutableSecondPersonWithGeneratedId())); - ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties = - new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel1", new ImmutablePersonWithGeneratedId()); + ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties = new ImmutablePersonWithGeneratedIdRelationshipProperties( + null, "rel1", new ImmutablePersonWithGeneratedId()); - List relationshipPropertiesCollection = - Collections.singletonList(new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel2", new ImmutablePersonWithGeneratedId())); + List relationshipPropertiesCollection = Collections + .singletonList(new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel2", + new ImmutablePersonWithGeneratedId())); - Map relationshipPropertiesDynamic = - Collections.singletonMap("Ok", new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel3", new ImmutablePersonWithGeneratedId())); + Map relationshipPropertiesDynamic = Collections + .singletonMap("Ok", new ImmutablePersonWithGeneratedIdRelationshipProperties(null, "rel3", + new ImmutablePersonWithGeneratedId())); - Map> relationshipPropertiesDynamicCollection = - Collections.singletonMap("Nope", - Collections.singletonList(new ImmutableSecondPersonWithGeneratedIdRelationshipProperties( - null, "rel4", new ImmutableSecondPersonWithGeneratedId())) - ); + Map> relationshipPropertiesDynamicCollection = Collections + .singletonMap("Nope", + Collections.singletonList(new ImmutableSecondPersonWithGeneratedIdRelationshipProperties(null, + "rel4", new ImmutableSecondPersonWithGeneratedId()))); - ImmutablePersonWithGeneratedId person = new ImmutablePersonWithGeneratedId(null, - wasOnboardedBy, - knownBy, - ratedBy, - ratedByCollection, - fallback, - relationshipProperties, - relationshipPropertiesCollection, - relationshipPropertiesDynamic, - relationshipPropertiesDynamicCollection - ); + ImmutablePersonWithGeneratedId person = new ImmutablePersonWithGeneratedId(null, wasOnboardedBy, knownBy, + ratedBy, ratedByCollection, fallback, relationshipProperties, relationshipPropertiesCollection, + relationshipPropertiesDynamic, relationshipPropertiesDynamicCollection); - StepVerifier.create(repository.save(person)) - .assertNext(savedPerson -> { + StepVerifier.create(repository.save(person)).assertNext(savedPerson -> { - assertThat(person.id).isNull(); - assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); - assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); + assertThat(person.id).isNull(); + assertThat(savedPerson.wasOnboardedBy.get(0).id).isNotNull(); + assertThat(savedPerson.knownBy.iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); - assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); + assertThat(savedPerson.ratedBy.keySet().iterator().next()).isEqualTo("Good"); + assertThat(savedPerson.ratedBy.values().iterator().next().id).isNotNull(); - assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); - assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); + assertThat(savedPerson.ratedByCollection.keySet().iterator().next()).isEqualTo("Na"); + assertThat(savedPerson.ratedByCollection.values().iterator().next().get(0).id).isNotNull(); - assertThat(savedPerson.fallback.id).isNotNull(); + assertThat(savedPerson.fallback.id).isNotNull(); - assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); - assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); + assertThat(savedPerson.relationshipProperties.name).isEqualTo("rel1"); + assertThat(savedPerson.relationshipProperties.target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); - assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).name).isEqualTo("rel2"); + assertThat(savedPerson.relationshipPropertiesCollection.get(0).target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); - assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); + assertThat(savedPerson.relationshipPropertiesDynamic.keySet().iterator().next()).isEqualTo("Ok"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().name).isEqualTo("rel3"); + assertThat(savedPerson.relationshipPropertiesDynamic.values().iterator().next().target.id).isNotNull(); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()).isEqualTo("Nope"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name).isEqualTo("rel4"); - assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id).isNotNull(); - }) - .verifyComplete(); - } - - interface ReactiveImmutablePersonWithGeneratedIdRepository extends ReactiveNeo4jRepository { + assertThat(savedPerson.relationshipPropertiesDynamicCollection.keySet().iterator().next()) + .isEqualTo("Nope"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).name) + .isEqualTo("rel4"); + assertThat(savedPerson.relationshipPropertiesDynamicCollection.values().iterator().next().get(0).target.id) + .isNotNull(); + }).verifyComplete(); } @Test // GH-2148 @@ -313,15 +299,13 @@ public class ReactiveImmutableGeneratedIdsIT { List children = Arrays.asList(new MutableChild(), new MutableChild()); parent.setChildren(children); - template.save(parent).as(StepVerifier::create) - .consumeNextWith(saved -> { - assertThat(saved).isSameAs(parent); - assertThat(saved.getId()).isNotNull(); - assertThat(saved.getChildren()).isSameAs(children); - assertThat(saved.getChildren()).allMatch(c -> c.getId() != null && children.contains(c)); + template.save(parent).as(StepVerifier::create).consumeNextWith(saved -> { + assertThat(saved).isSameAs(parent); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getChildren()).isSameAs(children); + assertThat(saved.getChildren()).allMatch(c -> c.getId() != null && children.contains(c)); - }) - .verifyComplete(); + }).verifyComplete(); } @Test // GH-2223 @@ -336,53 +320,63 @@ public class ReactiveImmutableGeneratedIdsIT { onboardedBy.add(person2); ImmutablePersonWithGeneratedId person3 = ImmutablePersonWithGeneratedId.wasOnboardedBy(onboardedBy); - StepVerifier.create(repository.save(person3)) - .assertNext(savedPerson -> { - assertThat(savedPerson.id).isNotNull(); - assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); + StepVerifier.create(repository.save(person3)).assertNext(savedPerson -> { + assertThat(savedPerson.id).isNotNull(); + assertThat(savedPerson.wasOnboardedBy).allMatch(ob -> ob.id != null); - ImmutablePersonWithGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream().filter(p -> p.fallback != null).findFirst().get(); - assertThat(savedPerson2.fallback.id).isNotNull(); - }) - .verifyComplete(); + ImmutablePersonWithGeneratedId savedPerson2 = savedPerson.wasOnboardedBy.stream() + .filter(p -> p.fallback != null) + .findFirst() + .get(); + assertThat(savedPerson2.fallback.id).isNotNull(); + }).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run( - "MATCH (person3:ImmutablePersonWithGeneratedId) " + - "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithGeneratedId) " + - "-[:FALLBACK]->(person1:ImmutablePersonWithGeneratedId), " + - "(person3)-[:ONBOARDED_BY]->(person1) " + - "return person3") - .list(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + List result = session + .run("MATCH (person3:ImmutablePersonWithGeneratedId) " + + "-[:ONBOARDED_BY]->(person2:ImmutablePersonWithGeneratedId) " + + "-[:FALLBACK]->(person1:ImmutablePersonWithGeneratedId), " + + "(person3)-[:ONBOARDED_BY]->(person1) " + "return person3") + .list(); assertThat(result).hasSize(1); } } + interface ReactiveImmutablePersonWithGeneratedIdRepository + extends ReactiveNeo4jRepository { + + } + @Configuration @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) @EnableTransactionManagement static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java index 67965a9e3..778aa660a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; import java.util.List; @@ -47,6 +45,11 @@ import org.neo4j.driver.reactivestreams.ReactiveResult; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.summary.ResultSummary; import org.reactivestreams.Publisher; +import reactor.blockhound.BlockHound; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,10 +67,7 @@ import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; -import reactor.blockhound.BlockHound; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -85,93 +85,96 @@ class ReactiveNeo4jClientIT { @BeforeEach void setupData(@Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) { - try ( - Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.commit(); } } @Test // GH-2755 - public void testQueryExecutionNeo4jClient(@Autowired ReactiveNeo4jClient neo4jClient, @Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { + void testQueryExecutionNeo4jClient(@Autowired ReactiveNeo4jClient neo4jClient, @Autowired Driver driver, + @Autowired BookmarkCapture bookmarkCapture) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - session.run("UNWIND range(1,10000) as count with count CREATE (u:VersionedExternalIdListBased) SET u.numberThing=count").consume(); + session.run( + "UNWIND range(1,10000) as count with count CREATE (u:VersionedExternalIdListBased) SET u.numberThing=count") + .consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } String cypher = "MATCH (n) RETURN elementId(n)"; String cypher2 = "MATCH (n) WHERE elementId(n) = $elementId RETURN elementId(n)"; - StepVerifier.create(neo4jClient.query(cypher).fetchAs(String.class).all() - .flatMap(elementId -> - neo4jClient.query(cypher2) - .bindAll(Map.of("elementId", elementId)) - .fetchAs(String.class).one())) - .expectNextCount(10000) - .verifyComplete(); + StepVerifier + .create(neo4jClient.query(cypher) + .fetchAs(String.class) + .all() + .flatMap(elementId -> neo4jClient.query(cypher2) + .bindAll(Map.of("elementId", elementId)) + .fetchAs(String.class) + .one())) + .expectNextCount(10000) + .verifyComplete(); } @Test // GH-2755 - public void testQueryExecutionPureDriver(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { + void testQueryExecutionPureDriver(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { try (var session = driver.session(bookmarkCapture.createSessionConfig())) { - session.run("UNWIND range(1,10000) as count with count CREATE (u:VersionedExternalIdListBased) SET u.numberThing=count").consume(); + session.run( + "UNWIND range(1,10000) as count with count CREATE (u:VersionedExternalIdListBased) SET u.numberThing=count") + .consume(); bookmarkCapture.seedWith(session.lastBookmarks()); } String cypher = "MATCH (n) RETURN elementId(n) as a"; String cypher2 = "MATCH (n) WHERE elementId(n) = $elementId RETURN elementId(n) as b"; - StepVerifier.create(Flux.usingWhen( - Mono - .just(driver.session(ReactiveSession.class)), - session -> - Flux.from(session.run(cypher)) + StepVerifier.create(Flux.usingWhen(Mono.just(driver.session(ReactiveSession.class)), + session -> Flux.from(session.run(cypher)) + .flatMap(ReactiveResult::records) + .map(a -> a.get(0).asString()) + .flatMap(elementId -> Flux.usingWhen(Mono.just(driver.session(ReactiveSession.class)), + innerSession -> Flux.from(innerSession.run(cypher2, Map.of("elementId", elementId))) .flatMap(ReactiveResult::records) - .map(a -> a.get(0).asString()) - .flatMap(elementId -> - Flux.usingWhen( - Mono.just(driver.session(ReactiveSession.class)), - innerSession -> - Flux.from(innerSession.run(cypher2, Map.of("elementId", elementId))) - .flatMap(ReactiveResult::records) - .map(result -> result.get(0).asString()), - innerSession -> Mono.fromDirect(innerSession.close()) - )), + .map(result -> result.get(0).asString()), + innerSession -> Mono.fromDirect(innerSession.close()))), session -> Mono.fromDirect(session.close()))) - .expectNextCount(10000) - .verifyComplete(); + .expectNextCount(10000) + .verifyComplete(); } @Test // GH-2238 void clientShouldIntegrateWithCypherDSL(@Autowired TransactionalOperator transactionalOperator, - @Autowired ReactiveNeo4jClient client, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired ReactiveNeo4jClient client, @Autowired BookmarkCapture bookmarkCapture) { - Node namedAnswer = Cypher.node("TheAnswer", Cypher.mapOf("value", - Cypher.literalOf(23).multiply(Cypher.literalOf(2)).subtract(Cypher.literalOf(4)))).named("n"); + Node namedAnswer = Cypher + .node("TheAnswer", + Cypher.mapOf("value", + Cypher.literalOf(23).multiply(Cypher.literalOf(2)).subtract(Cypher.literalOf(4)))) + .named("n"); NewReactiveExecutableResultStatement statement = new NewReactiveExecutableResultStatement(namedAnswer); AtomicLong vanishedId = new AtomicLong(); transactionalOperator.execute(transaction -> { - Flux inner = client.getQueryRunner() - .flatMapMany(statement::fetchWith) - .doOnNext(r -> vanishedId.set(TestIdentitySupport.getInternalId(r.get("n").asNode()))) - .map(record -> record.get("n").get("value").asLong()); + Flux inner = client.getQueryRunner() + .flatMapMany(statement::fetchWith) + .doOnNext(r -> vanishedId.set(TestIdentitySupport.getInternalId(r.get("n").asNode()))) + .map(record -> record.get("n").get("value").asLong()); - transaction.setRollbackOnly(); - return inner; - }).as(StepVerifier::create) - .expectNext(42L) - .verifyComplete(); + transaction.setRollbackOnly(); + return inner; + }).as(StepVerifier::create).expectNext(42L).verifyComplete(); - // Make sure we actually interacted with the managed transaction (that had been rolled back) + // Make sure we actually interacted with the managed transaction (that had been + // rolled back) try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run("MATCH (n) WHERE id(n) = $id RETURN count(n)", - Collections.singletonMap("id", vanishedId.get())).single().get(0).asLong(); + long cnt = session + .run("MATCH (n) WHERE id(n) = $id RETURN count(n)", Collections.singletonMap("id", vanishedId.get())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(0L); } } @@ -181,17 +184,19 @@ class ReactiveNeo4jClientIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } - @Override // needed here because there is no implicit registration of entities upfront some methods under test + @Override // needed here because there is no implicit registration of entities + // upfront some methods under test protected Collection getMappingBasePackages() { return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -205,7 +210,7 @@ class ReactiveNeo4jClientIT { } @Bean - public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { + TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { return TransactionalOperator.create(transactionManager); } @@ -213,6 +218,7 @@ class ReactiveNeo4jClientIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } private static class NewReactiveExecutableResultStatement implements ExecutableResultStatement { @@ -220,9 +226,7 @@ class ReactiveNeo4jClientIT { private final Statement delegate; NewReactiveExecutableResultStatement(Node namedAnswer) { - delegate = Cypher.create(namedAnswer) - .returning(namedAnswer) - .build(); + this.delegate = Cypher.create(namedAnswer).returning(namedAnswer).build(); } /** @@ -230,9 +234,10 @@ class ReactiveNeo4jClientIT { * @param reactiveQueryRunner The runner to run the statement with * @return a publisher of records */ - public Publisher fetchWith(ReactiveQueryRunner reactiveQueryRunner) { - return Mono.fromCallable(this::createQuery).flatMapMany(reactiveQueryRunner::run) - .flatMap(ReactiveResult::records); + Publisher fetchWith(ReactiveQueryRunner reactiveQueryRunner) { + return Mono.fromCallable(this::createQuery) + .flatMapMany(reactiveQueryRunner::run) + .flatMap(ReactiveResult::records); } @Override @@ -240,7 +245,8 @@ class ReactiveNeo4jClientIT { throw new UnsupportedOperationException(); } - @Override public CompletableFuture> fetchWith(AsyncQueryRunner asyncQueryRunner, + @Override + public CompletableFuture> fetchWith(AsyncQueryRunner asyncQueryRunner, Function function) { throw new UnsupportedOperationException(); } @@ -278,5 +284,7 @@ class ReactiveNeo4jClientIT { public String getCypher() { return this.delegate.getCypher(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTemplateIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTemplateIT.java index e54530529..e122a42d3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTemplateIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTemplateIT.java @@ -15,6 +15,17 @@ */ package org.springframework.data.neo4j.integration.reactive; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -28,6 +39,9 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,18 +71,6 @@ import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BiPredicate; -import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.neo4j.cypherdsl.core.Cypher.parameter; @@ -80,17 +82,23 @@ import static org.neo4j.cypherdsl.core.Cypher.parameter; @Neo4jIntegrationTest @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) class ReactiveNeo4jTemplateIT { + private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; protected static Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; + private final ReactiveNeo4jTemplate neo4jTemplate; private Long person1Id; + private Long person2Id; + private Long simonsId; + private Long nullNullSchneider; @Autowired @@ -99,40 +107,55 @@ class ReactiveNeo4jTemplateIT { this.neo4jTemplate = neo4jTemplate; } + private static BiPredicate create2LevelProjectingPredicate() { + BiPredicate predicate = (path, property) -> false; + predicate = predicate.or((path, property) -> property.getName().equals("lastName")); + predicate = predicate.or((path, property) -> property.getName().equals("address") + || path.toDotPath().startsWith("address.") && property.getName().equals("street")); + predicate = predicate.or((path, property) -> property.getName().equals("country") + || path.toDotPath().contains("address.country.") && property.getName().equals("name")); + return predicate; + } + @BeforeEach void setupData(@Autowired BookmarkCapture bookmarkCapture) { - try ( - Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction(); - ) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction();) { transaction.run("MATCH (n) detach delete n"); - person1Id = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", - Values.parameters("name", TEST_PERSON1_NAME)).single().get("id").asLong(); - person2Id = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", - Values.parameters("name", TEST_PERSON2_NAME)).single().get("id").asLong(); + this.person1Id = transaction + .run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", + Values.parameters("name", TEST_PERSON1_NAME)) + .single() + .get("id") + .asLong(); + this.person2Id = transaction + .run("CREATE (n:PersonWithAllConstructor) SET n.name = $name RETURN id(n) AS id", + Values.parameters("name", TEST_PERSON2_NAME)) + .single() + .get("id") + .asLong(); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); - simonsId = transaction - .run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" + - "-[:LIVES_AT]->(a:Address {city: 'Aachen', id: 1})" + - "-[:BASED_IN]->(c:YetAnotherCountryEntity{name: 'Gemany', countryCode: 'DE'})" + - "RETURN id(p)") - .single().get(0).asLong(); - nullNullSchneider = transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schnitzel'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'}) RETURN id(p)") - .single().get(0).asLong(); + this.simonsId = transaction.run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" + + "-[:LIVES_AT]->(a:Address {city: 'Aachen', id: 1})" + + "-[:BASED_IN]->(c:YetAnotherCountryEntity{name: 'Gemany', countryCode: 'DE'})" + "RETURN id(p)") + .single() + .get(0) + .asLong(); + this.nullNullSchneider = transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schnitzel'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'}) RETURN id(p)") + .single() + .get(0) + .asLong(); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.run("CREATE (p:PersonWithAssignedId{id: 'x', firstName: 'John', lastName: 'Doe'})"); - transaction.run( - "CREATE (root:NodeEntity:BaseNodeEntity{nodeId: 'root'}) " + - "CREATE (company:NodeEntity:BaseNodeEntity{nodeId: 'comp'}) " + - "CREATE (cred:Credential{id: 'uuid-1', name: 'Creds'}) " + - "CREATE (company)-[:CHILD_OF]->(root) " + - "CREATE (root)-[:HAS_CREDENTIAL]->(cred) " + - "CREATE (company)-[:WITH_CREDENTIAL]->(cred)"); + transaction.run("CREATE (root:NodeEntity:BaseNodeEntity{nodeId: 'root'}) " + + "CREATE (company:NodeEntity:BaseNodeEntity{nodeId: 'comp'}) " + + "CREATE (cred:Credential{id: 'uuid-1', name: 'Creds'}) " + "CREATE (company)-[:CHILD_OF]->(root) " + + "CREATE (root)-[:HAS_CREDENTIAL]->(cred) " + "CREATE (company)-[:WITH_CREDENTIAL]->(cred)"); transaction.commit(); @@ -142,8 +165,9 @@ class ReactiveNeo4jTemplateIT { @Test void count() { - StepVerifier.create(neo4jTemplate.count(PersonWithAllConstructor.class)) - .assertNext(count -> assertThat(count).isEqualTo(2)).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(PersonWithAllConstructor.class)) + .assertNext(count -> assertThat(count).isEqualTo(2)) + .verifyComplete(); } @Test @@ -151,18 +175,22 @@ class ReactiveNeo4jTemplateIT { Node node = Cypher.node("PersonWithAllConstructor").named("n"); Statement statement = Cypher.match(node).returning(Cypher.count(node)).build(); - StepVerifier.create(neo4jTemplate.count(statement)).assertNext(count -> assertThat(count).isEqualTo(2)) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(statement)) + .assertNext(count -> assertThat(count).isEqualTo(2)) + .verifyComplete(); } @Test void countWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(parameter("name"))) - .returning(Cypher.count(node)).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(parameter("name"))) + .returning(Cypher.count(node)) + .build(); - StepVerifier.create(neo4jTemplate.count(statement, Collections.singletonMap("name", TEST_PERSON1_NAME))) - .assertNext(count -> assertThat(count).isEqualTo(1)).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(statement, Collections.singletonMap("name", TEST_PERSON1_NAME))) + .assertNext(count -> assertThat(count).isEqualTo(1)) + .verifyComplete(); } @Test @@ -170,21 +198,25 @@ class ReactiveNeo4jTemplateIT { String cypherQuery = "MATCH (p:PersonWithAllConstructor) return count(p)"; - StepVerifier.create(neo4jTemplate.count(cypherQuery)).assertNext(count -> assertThat(count).isEqualTo(2)) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(cypherQuery)) + .assertNext(count -> assertThat(count).isEqualTo(2)) + .verifyComplete(); } @Test void countWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return count(p)"; - StepVerifier.create(neo4jTemplate.count(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME))) - .assertNext(count -> assertThat(count).isEqualTo(1)).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME))) + .assertNext(count -> assertThat(count).isEqualTo(1)) + .verifyComplete(); } @Test void findAll() { - StepVerifier.create(neo4jTemplate.findAll(PersonWithAllConstructor.class)).expectNextCount(2).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.findAll(PersonWithAllConstructor.class)) + .expectNextCount(2) + .verifyComplete(); } @Test @@ -192,75 +224,95 @@ class ReactiveNeo4jTemplateIT { Node node = Cypher.node("PersonWithAllConstructor").named("n"); Statement statement = Cypher.match(node).returning(node).build(); - StepVerifier.create(neo4jTemplate.findAll(statement, PersonWithAllConstructor.class)).expectNextCount(2) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.findAll(statement, PersonWithAllConstructor.class)) + .expectNextCount(2) + .verifyComplete(); } @Test void findAllWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(parameter("name"))) - .returning(node) - .build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(parameter("name"))) + .returning(node) + .build(); - StepVerifier.create(neo4jTemplate.findAll(statement, Collections.singletonMap("name", TEST_PERSON1_NAME), - PersonWithAllConstructor.class)).expectNextCount(1).verifyComplete(); + StepVerifier + .create(this.neo4jTemplate.findAll(statement, Collections.singletonMap("name", TEST_PERSON1_NAME), + PersonWithAllConstructor.class)) + .expectNextCount(1) + .verifyComplete(); } @Test void findOneWithStatementAndParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(parameter("name"))) - .returning(node) - .build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(parameter("name"))) + .returning(node) + .build(); - StepVerifier.create(neo4jTemplate.findOne(statement, Collections.singletonMap("name", TEST_PERSON1_NAME), - PersonWithAllConstructor.class)).expectNextCount(1).verifyComplete(); + StepVerifier + .create(this.neo4jTemplate.findOne(statement, Collections.singletonMap("name", TEST_PERSON1_NAME), + PersonWithAllConstructor.class)) + .expectNextCount(1) + .verifyComplete(); } @Test void findAllWithCypherQuery() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) return p"; - StepVerifier.create(neo4jTemplate.findAll(cypherQuery, PersonWithAllConstructor.class)).expectNextCount(2) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.findAll(cypherQuery, PersonWithAllConstructor.class)) + .expectNextCount(2) + .verifyComplete(); } @Test void findAllWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return p"; - StepVerifier.create(neo4jTemplate.findAll(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), - PersonWithAllConstructor.class)).expectNextCount(1).verifyComplete(); + StepVerifier + .create(this.neo4jTemplate.findAll(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), + PersonWithAllConstructor.class)) + .expectNextCount(1) + .verifyComplete(); } @Test void findOneWithCypherQueryAndParameters() { String cypherQuery = "MATCH (p:PersonWithAllConstructor) WHERE p.name = $name return p"; - StepVerifier.create(neo4jTemplate.findOne(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), - PersonWithAllConstructor.class)).expectNextCount(1).verifyComplete(); + StepVerifier + .create(this.neo4jTemplate.findOne(cypherQuery, Collections.singletonMap("name", TEST_PERSON1_NAME), + PersonWithAllConstructor.class)) + .expectNextCount(1) + .verifyComplete(); } @Test void findById() { - StepVerifier.create(neo4jTemplate.findById(person1Id, PersonWithAllConstructor.class)).expectNextCount(1) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.findById(this.person1Id, PersonWithAllConstructor.class)) + .expectNextCount(1) + .verifyComplete(); } @Test void findAllById() { StepVerifier - .create(neo4jTemplate.findAllById(Arrays.asList(person1Id, person2Id), PersonWithAllConstructor.class)) - .expectNextCount(2).verifyComplete(); + .create(this.neo4jTemplate.findAllById(Arrays.asList(this.person1Id, this.person2Id), + PersonWithAllConstructor.class)) + .expectNextCount(2) + .verifyComplete(); } @Test void save(@Autowired BookmarkCapture bookmarkCapture) { - StepVerifier.create(neo4jTemplate.save(new ThingWithGeneratedId("testThing"))).expectNextCount(1) - .verifyComplete(); + StepVerifier.create(this.neo4jTemplate.save(new ThingWithGeneratedId("testThing"))) + .expectNextCount(1) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (t:ThingWithGeneratedId{name: 'testThing'}) return t"); Value resultValue = result.single().get("t"); assertThat(resultValue).isNotNull(); @@ -275,42 +327,48 @@ class ReactiveNeo4jTemplateIT { ThingWithGeneratedId thing1 = new ThingWithGeneratedId(thing1Name); ThingWithGeneratedId thing2 = new ThingWithGeneratedId(thing2Name); - StepVerifier.create(neo4jTemplate.saveAll(Arrays.asList(thing1, thing2))).expectNextCount(2).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.saveAll(Arrays.asList(thing1, thing2))) + .expectNextCount(2) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Map paramMap = new HashMap<>(); paramMap.put("name1", thing1Name); paramMap.put("name2", thing2Name); Result result = session - .run("MATCH (t:ThingWithGeneratedId) WHERE t.name = $name1 or t.name = $name2 return t", - paramMap); + .run("MATCH (t:ThingWithGeneratedId) WHERE t.name = $name1 or t.name = $name2 return t", paramMap); List resultValues = result.list(); assertThat(resultValues).hasSize(2); - assertThat(resultValues).allMatch( - record -> record.asMap(Function.identity()).get("t").get("name").asString() - .startsWith("testThing")); + assertThat(resultValues).allMatch(record -> record.asMap(Function.identity()) + .get("t") + .get("name") + .asString() + .startsWith("testThing")); } } @Test - // 2230 + // 2230 void findAllWithStatementWithoutParameters() { Node node = Cypher.node("PersonWithAllConstructor").named("n"); - Statement statement = Cypher.match(node).where(node.property("name").isEqualTo(Cypher.parameter("name").withValue(TEST_PERSON1_NAME))) - .returning(node).build(); + Statement statement = Cypher.match(node) + .where(node.property("name").isEqualTo(Cypher.parameter("name").withValue(TEST_PERSON1_NAME))) + .returning(node) + .build(); - neo4jTemplate.findAll(statement, PersonWithAllConstructor.class) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + this.neo4jTemplate.findAll(statement, PersonWithAllConstructor.class) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); } @Test void deleteById(@Autowired BookmarkCapture bookmarkCapture) { - StepVerifier.create(neo4jTemplate.deleteById(person1Id, PersonWithAllConstructor.class)).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.deleteById(this.person1Id, PersonWithAllConstructor.class)) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithAllConstructor) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(1); } @@ -320,27 +378,581 @@ class ReactiveNeo4jTemplateIT { void deleteAllById(@Autowired BookmarkCapture bookmarkCapture) { StepVerifier - .create(neo4jTemplate - .deleteAllById(Arrays.asList(person1Id, person2Id), PersonWithAllConstructor.class)) - .verifyComplete(); + .create(this.neo4jTemplate.deleteAllById(Arrays.asList(this.person1Id, this.person2Id), + PersonWithAllConstructor.class)) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Result result = session.run("MATCH (p:PersonWithAllConstructor) return count(p) as count"); assertThat(result.single().get("count").asLong()).isEqualTo(0); } } + @Test + void saveAsWithOpenProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + // Using a query on purpose so that the address is null + template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .flatMap(p -> { + p.setFirstName("Micha"); + p.setLastName("Simons"); + return template.saveAs(p, OpenProjection.class); + }) + .map(OpenProjection::getFullName) + .as(StepVerifier::create) + .expectNext("Michael Simons") + .verifyComplete(); + + template.findById(this.simonsId, Person.class).as(StepVerifier::create).consumeNextWith(p -> { + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNotNull(); + }).verifyComplete(); + } + + @Test + // GH-2215 + void saveProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Siemons")) + .one() + .flatMap(p -> { + p.setFirstName("Micha"); + p.setLastName("Simons"); + return template.save(Person.class).one(p); + }) + .doOnNext(signal -> { + assertThat(signal.getFirstName()).isEqualTo("Micha"); + assertThat(signal.getLastName()).isEqualTo("Simons"); + }) + .flatMap(savedProjection -> template.findById(savedProjection.getId(), Person.class)) + .as(StepVerifier::create) + .expectNextMatches(person -> person.getFirstName().equals("Micha") && person.getLastName().equals("Simons") + && person.getAddress() != null) + .verifyComplete(); + } + + @Test + // GH-2215 + void saveAllProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Siemons")) + .one() + .flatMapMany(p -> { + p.setFirstName("Micha"); + p.setLastName("Simons"); + return template.save(Person.class).all(Collections.singleton(p)); + }) + .doOnNext(signal -> { + assertThat(signal.getFirstName()).isEqualTo("Micha"); + assertThat(signal.getLastName()).isEqualTo("Simons"); + }) + .flatMap(savedProjection -> template.findById(savedProjection.getId(), Person.class)) + .as(StepVerifier::create) + .expectNextMatches(person -> person.getFirstName().equals("Micha") && person.getLastName().equals("Simons") + && person.getAddress() != null) + .verifyComplete(); + } + + @Test + void saveAllAsWithOpenProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template, + @Autowired ReactiveTransactionManager transactionManager) { + + // Using a query on purpose so that the address is null + TransactionalOperator.create(transactionManager) + .transactional(template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class)) + .flatMapMany(t -> { + Person p1 = t.getT1(); + Person p2 = t.getT2(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + return template.saveAllAs(Arrays.asList(p1, p2), OpenProjection.class); + })) + .map(OpenProjection::getFullName) + .sort() + .as(StepVerifier::create) + .expectNext("Helge Schneider", "Michael Simons") + .verifyComplete(); + + template.findAllById(Arrays.asList(this.simonsId, this.nullNullSchneider), Person.class) + .collectList() + .as(StepVerifier::create) + .consumeNextWith(people -> { + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() != null); + }) + .verifyComplete(); + } + + @Test + void saveAsWithSameClassShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + // Using a query on purpose so that the address is null + template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .flatMap(p -> { + p.setFirstName("Micha"); + p.setLastName("Simons"); + return template.saveAs(p, Person.class); + }) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Micha") + .verifyComplete(); + + template.findById(this.simonsId, Person.class).as(StepVerifier::create).consumeNextWith(p -> { + assertThat(p.getFirstName()).isEqualTo("Micha"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNull(); + }).verifyComplete(); + } + + @Test + void saveAllAsWithSameClassShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + // Using a query on purpose so that the address is null + template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class)) + .flatMapMany(t -> { + Person p1 = t.getT1(); + Person p2 = t.getT2(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + return template.saveAllAs(Arrays.asList(p1, p2), Person.class); + }) + .map(Person::getLastName) + .sort() + .as(StepVerifier::create) + .expectNext("Schneider", "Simons") + .verifyComplete(); + + template.findAllById(Arrays.asList(this.simonsId, this.nullNullSchneider), Person.class) + .collectList() + .as(StepVerifier::create) + .consumeNextWith(people -> { + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Micha", "Helga"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() == null); + }) + .verifyComplete(); + } + + @Test + void saveAsWithClosedProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + // Using a query on purpose so that the address is null + template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .flatMap(p -> { + p.setFirstName("Micha"); + p.setLastName("Simons"); + return template.saveAs(p, ClosedProjection.class); + }) + .map(ClosedProjection::getLastName) + .as(StepVerifier::create) + .expectNext("Simons") + .verifyComplete(); + + template.findById(this.simonsId, Person.class).as(StepVerifier::create).consumeNextWith(p -> { + assertThat(p.getFirstName()).isEqualTo("Michael"); + assertThat(p.getLastName()).isEqualTo("Simons"); + assertThat(p.getAddress()).isNotNull(); + }).verifyComplete(); + } + + @Test + void saveAsWithClosedProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .flatMapMany(p -> { + + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + return this.neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); + }) + .as(StepVerifier::create) + .assertNext(projection -> { + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + }) + .verifyComplete(); + template.findById(this.simonsId, Person.class).as(StepVerifier::create).assertNext(p -> { + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + }).verifyComplete(); + } + + @Test + // GH-2420 + void saveAsWithDynamicProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .flatMapMany(p -> { + + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + Person.Address.Country country = new Person.Address.Country(); + country.setName("Foo"); + country.setCountryCode("DE"); + p.getAddress().setCountry(country); + return this.neo4jTemplate.saveAs(p, create2LevelProjectingPredicate()); + }) + .as(StepVerifier::create) + .assertNext(projection -> { + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + }) + .verifyComplete(); + template.findById(this.simonsId, Person.class).as(StepVerifier::create).assertNext(p -> { + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); + }).verifyComplete(); + } + + @Test + // GH-2420 + void saveAllAsWithDynamicProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template + .findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .flatMapMany(p -> { + + p.getAddress().setCity("Braunschweig"); + p.getAddress().setStreet("Single Trail"); + Person.Address.Country country = new Person.Address.Country(); + country.setName("Foo"); + country.setCountryCode("DE"); + p.getAddress().setCountry(country); + return this.neo4jTemplate.saveAllAs(Collections.singletonList(p), create2LevelProjectingPredicate()); + }) + .as(StepVerifier::create) + .assertNext(projection -> { + assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); + }) + .verifyComplete(); + template.findById(this.simonsId, Person.class).as(StepVerifier::create).assertNext(p -> { + assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); + assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); + assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); + }).verifyComplete(); + } + + @Test + void saveAsWithClosedProjectionOnThreeLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + template.findOne( + "MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", + Collections.singletonMap("lastName", "Siemons"), Person.class) + .flatMapMany(p -> { + + Person.Address.Country country = p.getAddress().getCountry(); + country.setName("Germany"); + country.setCountryCode("AT"); + return this.neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); + }) + .as(StepVerifier::create) + .assertNext(p -> assertThat(p.getAddress().getCountry().getName()).isEqualTo("Germany")) + .verifyComplete(); + + template.findById(this.simonsId, Person.class).as(StepVerifier::create).assertNext(p -> { + Person.Address.Country savedCountry = p.getAddress().getCountry(); + assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); + assertThat(savedCountry.getName()).isEqualTo("Germany"); + }).verifyComplete(); + } + + @Test + // GH-2544 + void saveAllAsWithEmptyList(@Autowired ReactiveNeo4jTemplate template) { + + template.saveAllAs(Collections.emptyList(), ClosedProjection.class).as(StepVerifier::create).verifyComplete(); + } + + @Test + // GH-2544 + void saveWeirdHierarchy(@Autowired ReactiveNeo4jTemplate template) { + + List things = new ArrayList<>(); + things.add(new X()); + things.add(new Y()); + + template.saveAllAs(things, ClosedProjection.class) + .as(StepVerifier::create) + .verifyErrorMatches(t -> t instanceof IllegalArgumentException + && t.getMessage().equals("Could not determine a common element of an heterogeneous collection")); + } + + @Test + void saveAllAsWithClosedProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { + + // Using a query on purpose so that the address is null + template + .findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons"), + Person.class) + .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel"), Person.class)) + .flatMapMany(t -> { + Person p1 = t.getT1(); + Person p2 = t.getT2(); + + p1.setFirstName("Micha"); + p1.setLastName("Simons"); + + p2.setFirstName("Helga"); + p2.setLastName("Schneider"); + return template.saveAllAs(Arrays.asList(p1, p2), ClosedProjection.class); + }) + .map(ClosedProjection::getLastName) + .sort() + .as(StepVerifier::create) + .expectNext("Schneider", "Simons") + .verifyComplete(); + + template.findAllById(Arrays.asList(this.simonsId, this.nullNullSchneider), Person.class) + .collectList() + .as(StepVerifier::create) + .consumeNextWith(people -> { + assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); + assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); + assertThat(people).allMatch(p -> p.getAddress() != null); + }) + .verifyComplete(); + } + + @Test + void updatingFindShouldWork(@Autowired ReactiveTransactionManager transactionManager) { + Map params = new HashMap<>(); + params.put("wrongName", "Siemons"); + params.put("correctName", "Simons"); + TransactionalOperator.create(transactionManager) + .transactional(this.neo4jTemplate.findOne( + "MERGE (p:Person {lastName: $wrongName}) ON MATCH set p.lastName = $correctName RETURN p", params, + Person.class)) + .as(StepVerifier::create) + .consumeNextWith(updatedPerson -> { + + assertThat(updatedPerson.getLastName()).isEqualTo("Simons"); + assertThat(updatedPerson.getAddress()).isNull(); // We didn't fetch it + }) + .verifyComplete(); + } + + @Test + void executableFindShouldWorkAllDomainObjectsShouldWork() { + this.neo4jTemplate.find(Person.class).all().as(StepVerifier::create).expectNextCount(4L).verifyComplete(); + } + + @Test + void executableFindShouldWorkAllDomainObjectsProjectedShouldWork() { + this.neo4jTemplate.find(Person.class) + .as(OpenProjection.class) + .all() + .map(OpenProjection::getFullName) + .sort() + .as(StepVerifier::create) + .expectNext("A LA", "Bela B.", "Helge Schnitzel", "Michael Siemons") + .verifyComplete(); + } + + @Test + // GH-2270 + void executableFindShouldWorkAllDomainObjectsProjectedDTOShouldWork() { + + this.neo4jTemplate.find(Person.class) + .as(DtoPersonProjection.class) + .all() + .map(DtoPersonProjection::getLastName) + .sort() + .as(StepVerifier::create) + .expectNext("B.", "LA", "Schnitzel", "Siemons") + .verifyComplete(); + } + + @Test + // GH-2270 + void executableFindShouldWorkOneDomainObjectsProjectedDTOShouldWork() { + + this.neo4jTemplate.find(Person.class) + .as(DtoPersonProjection.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel")) + .one() + .map(DtoPersonProjection::getLastName) + .as(StepVerifier::create) + .expectNext("Schnitzel") + .verifyComplete(); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQuery() { + this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p LIMIT 1") + .all() + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQueryAndParam() { + this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", + Collections.singletonMap("lastName", "Schnitzel")) + .all() + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + } + + @Test + void executableFindShouldWorkDomainObjectsWithQueryAndNullParams() { + + this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p LIMIT 1", null) + .all() + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + } + + @Test + void oneShouldWork() { + this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p LIMIT 1") + .one() + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + } + + @Test + void oneShouldWorkWithIncorrectResultSize() { + this.neo4jTemplate.find(Person.class) + .matching("MATCH (p:Person) RETURN p") + .one() + .as(StepVerifier::create) + .verifyError(IncorrectResultSizeDataAccessException.class); + } + + @Test + void statementShouldWork() { + Node person = Cypher.node("Person"); + Flux people = this.neo4jTemplate.find(Person.class) + .matching(Cypher.match(person) + .where(person.property("lastName").isEqualTo(Cypher.anonParameter("Siemons"))) + .returning(person) + .build()) + .all(); + people.map(Person::getLastName).as(StepVerifier::create).expectNext("Siemons").verifyComplete(); + } + + @Test + void statementWithParamsShouldWork() { + Node person = Cypher.node("Person"); + Flux people = this.neo4jTemplate.find(Person.class) + .matching(Cypher.match(person) + .where(person.property("lastName").isEqualTo(Cypher.parameter("lastName", "Siemons"))) + .returning(person) + .build(), Collections.singletonMap("lastName", "Schnitzel")) + .all(); + people.map(Person::getLastName).as(StepVerifier::create).expectNext("Schnitzel").verifyComplete(); + } + + @Test + // GH-2407 + void shouldSaveAllAsWithAssignedIdProjected() { + + this.neo4jTemplate.findById("x", PersonWithAssignedId.class).flatMapMany(p -> { + p.setLastName("modifiedLast"); + p.setFirstName("modifiedFirst"); + return this.neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjection.class); + }).map(ClosedProjection::getLastName).as(StepVerifier::create).expectNext("modifiedLast").verifyComplete(); + + this.neo4jTemplate.findById("x", PersonWithAssignedId.class).as(StepVerifier::create).consumeNextWith(p -> { + assertThat(p.getFirstName()).isEqualTo("John"); + assertThat(p.getLastName()).isEqualTo("modifiedLast"); + }).verifyComplete(); + } + + @Test + // GH-2407 + void shouldSaveAsWithAssignedIdProjected() { + + this.neo4jTemplate.findById("x", PersonWithAssignedId.class).flatMap(p -> { + p.setLastName("modifiedLast"); + p.setFirstName("modifiedFirst"); + return this.neo4jTemplate.saveAs(p, ClosedProjection.class); + }).map(ClosedProjection::getLastName).as(StepVerifier::create).expectNext("modifiedLast").verifyComplete(); + + this.neo4jTemplate.findById("x", PersonWithAssignedId.class).as(StepVerifier::create).consumeNextWith(p -> { + assertThat(p.getFirstName()).isEqualTo("John"); + assertThat(p.getLastName()).isEqualTo("modifiedLast"); + }).verifyComplete(); + } + + @Test + // GH-2415 + void saveWithProjectionImplementedByEntity(@Autowired Neo4jMappingContext mappingContext) { + + Neo4jPersistentEntity metaData = mappingContext.getPersistentEntity(BaseNodeEntity.class); + this.neo4jTemplate.find(BaseNodeEntity.class) + .as(NodeEntity.class) + .matching(QueryFragmentsAndParameters.forCondition(metaData, + Constants.NAME_OF_TYPED_ROOT_NODE.apply(metaData) + .property("nodeId") + .isEqualTo(Cypher.literalOf("root")))) + .one() + .flatMap(nodeEntity -> this.neo4jTemplate.saveAs(nodeEntity, NodeWithDefinedCredentials.class)) + .flatMap(nodeEntity -> this.neo4jTemplate.findById(nodeEntity.getNodeId(), NodeEntity.class)) + .as(StepVerifier::create) + .consumeNextWith(nodeEntity -> assertThat(nodeEntity.getChildren()).hasSize(1)) + .verifyComplete(); + } + interface OpenProjection { String getLastName(); @org.springframework.beans.factory.annotation.Value("#{target.firstName + ' ' + target.lastName}") String getFullName(); + } interface ClosedProjection { String getLastName(); + } interface ClosedProjectionWithEmbeddedProjection { @@ -356,22 +968,16 @@ class ReactiveNeo4jTemplateIT { CountryProjection getCountry(); interface CountryProjection { + String getName(); + } + } + } - private static BiPredicate create2LevelProjectingPredicate() { - BiPredicate predicate = (path, property) -> false; - predicate = predicate.or((path, property) -> property.getName().equals("lastName")); - predicate = predicate.or((path, property) -> property.getName().equals("address") - || path.toDotPath().startsWith("address.") && property.getName().equals("street")); - predicate = predicate.or((path, property) -> property.getName().equals("country") - || path.toDotPath().contains("address.country.") && property.getName().equals("name")); - return predicate; - } - - static class DtoPersonProjection { + public static class DtoPersonProjection { /** * The ID is required in a project that should be saved. @@ -379,6 +985,7 @@ class ReactiveNeo4jTemplateIT { private final Long id; private String lastName; + private String firstName; DtoPersonProjection(Long id) { @@ -393,18 +1000,23 @@ class ReactiveNeo4jTemplateIT { return this.lastName; } - public String getFirstName() { - return this.firstName; - } - public void setLastName(String lastName) { this.lastName = lastName; } + public String getFirstName() { + return this.firstName; + } + public void setFirstName(String firstName) { this.firstName = firstName; } + protected boolean canEqual(final Object other) { + return other instanceof DtoPersonProjection; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -418,608 +1030,46 @@ class ReactiveNeo4jTemplateIT { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$lastName = this.getLastName(); final Object other$lastName = other.getLastName(); - if (this$lastName == null ? other$lastName != null : !this$lastName.equals(other$lastName)) { + if (!Objects.equals(this$lastName, other$lastName)) { return false; } final Object this$firstName = this.getFirstName(); final Object other$firstName = other.getFirstName(); - if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof DtoPersonProjection; + return Objects.equals(this$firstName, other$firstName); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $lastName = this.getLastName(); - result = result * PRIME + ($lastName == null ? 43 : $lastName.hashCode()); + result = result * PRIME + (($lastName != null) ? $lastName.hashCode() : 43); final Object $firstName = this.getFirstName(); - result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode()); + result = result * PRIME + (($firstName != null) ? $firstName.hashCode() : 43); return result; } + @Override public String toString() { - return "ReactiveNeo4jTemplateIT.DtoPersonProjection(id=" + this.getId() + ", lastName=" + this.getLastName() + ", firstName=" + this.getFirstName() + ")"; + return "ReactiveNeo4jTemplateIT.DtoPersonProjection(id=" + this.getId() + ", lastName=" + this.getLastName() + + ", firstName=" + this.getFirstName() + ")"; } - } - @Test - void saveAsWithOpenProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - // Using a query on purpose so that the address is null - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMap(p -> { - p.setFirstName("Micha"); - p.setLastName("Simons"); - return template.saveAs(p, OpenProjection.class); - }).map(OpenProjection::getFullName) - .as(StepVerifier::create) - .expectNext("Michael Simons") - .verifyComplete(); - - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .consumeNextWith(p -> { - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNotNull(); - }) - .verifyComplete(); - } - - @Test - // GH-2215 - void saveProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template - .find(Person.class) - .as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons")) - .one() - .flatMap(p -> { - p.setFirstName("Micha"); - p.setLastName("Simons"); - return template.save(Person.class).one(p); - }) - .doOnNext(signal -> { - assertThat(signal.getFirstName()).isEqualTo("Micha"); - assertThat(signal.getLastName()).isEqualTo("Simons"); - }) - .flatMap(savedProjection -> template.findById(savedProjection.getId(), Person.class)) - .as(StepVerifier::create) - .expectNextMatches( - person -> person.getFirstName().equals("Micha") && person.getLastName().equals("Simons") - && person.getAddress() != null) - .verifyComplete(); - } - - @Test - // GH-2215 - void saveAllProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template - .find(Person.class) - .as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Siemons")) - .one() - .flatMapMany(p -> { - p.setFirstName("Micha"); - p.setLastName("Simons"); - return template.save(Person.class).all(Collections.singleton(p)); - }) - .doOnNext(signal -> { - assertThat(signal.getFirstName()).isEqualTo("Micha"); - assertThat(signal.getLastName()).isEqualTo("Simons"); - }) - .flatMap(savedProjection -> template.findById(savedProjection.getId(), Person.class)) - .as(StepVerifier::create) - .expectNextMatches( - person -> person.getFirstName().equals("Micha") && person.getLastName().equals("Simons") - && person.getAddress() != null) - .verifyComplete(); - } - - @Test - void saveAllAsWithOpenProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template, @Autowired ReactiveTransactionManager transactionManager) { - - // Using a query on purpose so that the address is null - TransactionalOperator.create(transactionManager).transactional( - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class)) - .flatMapMany(t -> { - Person p1 = t.getT1(); - Person p2 = t.getT2(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - return template.saveAllAs(Arrays.asList(p1, p2), OpenProjection.class); - })) - .map(OpenProjection::getFullName) - .sort() - .as(StepVerifier::create) - .expectNext("Helge Schneider", "Michael Simons") - .verifyComplete(); - - template.findAllById(Arrays.asList(simonsId, nullNullSchneider), Person.class) - .collectList() - .as(StepVerifier::create) - .consumeNextWith(people -> { - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() != null); - }) - .verifyComplete(); - } - - @Test - void saveAsWithSameClassShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - // Using a query on purpose so that the address is null - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMap(p -> { - p.setFirstName("Micha"); - p.setLastName("Simons"); - return template.saveAs(p, Person.class); - }).map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Micha") - .verifyComplete(); - - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .consumeNextWith(p -> { - assertThat(p.getFirstName()).isEqualTo("Micha"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNull(); - }) - .verifyComplete(); - } - - @Test - void saveAllAsWithSameClassShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - // Using a query on purpose so that the address is null - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class)) - .flatMapMany(t -> { - Person p1 = t.getT1(); - Person p2 = t.getT2(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - return template.saveAllAs(Arrays.asList(p1, p2), Person.class); - }) - .map(Person::getLastName) - .sort() - .as(StepVerifier::create) - .expectNext("Schneider", "Simons") - .verifyComplete(); - - template.findAllById(Arrays.asList(simonsId, nullNullSchneider), Person.class) - .collectList() - .as(StepVerifier::create) - .consumeNextWith(people -> { - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Micha", "Helga"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() == null); - }) - .verifyComplete(); - } - - @Test - void saveAsWithClosedProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - // Using a query on purpose so that the address is null - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMap(p -> { - p.setFirstName("Micha"); - p.setLastName("Simons"); - return template.saveAs(p, ClosedProjection.class); - }).map(ClosedProjection::getLastName) - .as(StepVerifier::create) - .expectNext("Simons") - .verifyComplete(); - - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .consumeNextWith(p -> { - assertThat(p.getFirstName()).isEqualTo("Michael"); - assertThat(p.getLastName()).isEqualTo("Simons"); - assertThat(p.getAddress()).isNotNull(); - }) - .verifyComplete(); - } - - @Test - void saveAsWithClosedProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMapMany(p -> { - - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - return neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); - }) - .as(StepVerifier::create) - .assertNext(projection -> { - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - }) - .verifyComplete(); - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .assertNext(p -> { - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - }) - .verifyComplete(); - } - - @Test - // GH-2420 - void saveAsWithDynamicProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMapMany(p -> { - - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - Person.Address.Country country = new Person.Address.Country(); - country.setName("Foo"); - country.setCountryCode("DE"); - p.getAddress().setCountry(country); - return neo4jTemplate.saveAs(p, create2LevelProjectingPredicate()); - }) - .as(StepVerifier::create) - .assertNext(projection -> { - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - }) - .verifyComplete(); - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .assertNext(p -> { - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); - }) - .verifyComplete(); - } - - @Test - // GH-2420 - void saveAllAsWithDynamicProjectionOnSecondLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMapMany(p -> { - - p.getAddress().setCity("Braunschweig"); - p.getAddress().setStreet("Single Trail"); - Person.Address.Country country = new Person.Address.Country(); - country.setName("Foo"); - country.setCountryCode("DE"); - p.getAddress().setCountry(country); - return neo4jTemplate.saveAllAs(Collections.singletonList(p), create2LevelProjectingPredicate()); - }) - .as(StepVerifier::create) - .assertNext(projection -> { - assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"); - }) - .verifyComplete(); - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .assertNext(p -> { - assertThat(p.getAddress().getCity()).isEqualTo("Aachen"); - assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail"); - assertThat(p.getAddress().getCountry().getName()).isEqualTo("Foo"); - }) - .verifyComplete(); - } - - @Test - void saveAsWithClosedProjectionOnThreeLevelShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - template.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .flatMapMany(p -> { - - Person.Address.Country country = p.getAddress().getCountry(); - country.setName("Germany"); - country.setCountryCode("AT"); - return neo4jTemplate.saveAs(p, ClosedProjectionWithEmbeddedProjection.class); - }) - .as(StepVerifier::create) - .assertNext(p -> assertThat(p.getAddress().getCountry().getName()).isEqualTo("Germany")) - .verifyComplete(); - - template.findById(simonsId, Person.class) - .as(StepVerifier::create) - .assertNext(p -> { - Person.Address.Country savedCountry = p.getAddress().getCountry(); - assertThat(savedCountry.getCountryCode()).isEqualTo("DE"); - assertThat(savedCountry.getName()).isEqualTo("Germany"); - }) - .verifyComplete(); - } - - @Test - // GH-2544 - void saveAllAsWithEmptyList(@Autowired ReactiveNeo4jTemplate template) { - - template.saveAllAs(Collections.emptyList(), ClosedProjection.class) - .as(StepVerifier::create) - .verifyComplete(); } static class X { + } static class Y { - } - @Test - // GH-2544 - void saveWeirdHierarchy(@Autowired ReactiveNeo4jTemplate template) { - - List things = new ArrayList<>(); - things.add(new X()); - things.add(new Y()); - - template.saveAllAs(things, ClosedProjection.class) - .as(StepVerifier::create) - .verifyErrorMatches(t -> t instanceof IllegalArgumentException && t.getMessage().equals("Could not determine a common element of an heterogeneous collection")); - } - - @Test - void saveAllAsWithClosedProjectionShouldWork(@Autowired ReactiveNeo4jTemplate template) { - - // Using a query on purpose so that the address is null - template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Siemons"), Person.class) - .zipWith(template.findOne("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel"), Person.class)) - .flatMapMany(t -> { - Person p1 = t.getT1(); - Person p2 = t.getT2(); - - p1.setFirstName("Micha"); - p1.setLastName("Simons"); - - p2.setFirstName("Helga"); - p2.setLastName("Schneider"); - return template.saveAllAs(Arrays.asList(p1, p2), ClosedProjection.class); - }) - .map(ClosedProjection::getLastName) - .sort() - .as(StepVerifier::create) - .expectNext("Schneider", "Simons") - .verifyComplete(); - - template.findAllById(Arrays.asList(simonsId, nullNullSchneider), Person.class) - .collectList() - .as(StepVerifier::create) - .consumeNextWith(people -> { - assertThat(people).extracting(Person::getFirstName).containsExactlyInAnyOrder("Michael", "Helge"); - assertThat(people).extracting(Person::getLastName).containsExactlyInAnyOrder("Simons", "Schneider"); - assertThat(people).allMatch(p -> p.getAddress() != null); - }) - .verifyComplete(); - } - - @Test - void updatingFindShouldWork(@Autowired ReactiveTransactionManager transactionManager) { - Map params = new HashMap<>(); - params.put("wrongName", "Siemons"); - params.put("correctName", "Simons"); - TransactionalOperator.create(transactionManager) - .transactional( - neo4jTemplate - .findOne("MERGE (p:Person {lastName: $wrongName}) ON MATCH set p.lastName = $correctName RETURN p", - params, Person.class)) - .as(StepVerifier::create) - .consumeNextWith(updatedPerson -> { - - assertThat(updatedPerson.getLastName()).isEqualTo("Simons"); - assertThat(updatedPerson.getAddress()).isNull(); // We didn't fetch it - }) - .verifyComplete(); - } - - @Test - void executableFindShouldWorkAllDomainObjectsShouldWork() { - neo4jTemplate.find(Person.class).all().as(StepVerifier::create) - .expectNextCount(4L) - .verifyComplete(); - } - - @Test - void executableFindShouldWorkAllDomainObjectsProjectedShouldWork() { - neo4jTemplate.find(Person.class).as(OpenProjection.class).all() - .map(OpenProjection::getFullName) - .sort() - .as(StepVerifier::create) - .expectNext("A LA", "Bela B.", "Helge Schnitzel", "Michael Siemons") - .verifyComplete(); - } - - @Test - // GH-2270 - void executableFindShouldWorkAllDomainObjectsProjectedDTOShouldWork() { - - neo4jTemplate.find(Person.class).as(DtoPersonProjection.class).all() - .map(DtoPersonProjection::getLastName) - .sort() - .as(StepVerifier::create) - .expectNext("B.", "LA", "Schnitzel", "Siemons") - .verifyComplete(); - } - - @Test - // GH-2270 - void executableFindShouldWorkOneDomainObjectsProjectedDTOShouldWork() { - - neo4jTemplate.find(Person.class).as(DtoPersonProjection.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", Collections.singletonMap("lastName", "Schnitzel")) - .one() - .map(DtoPersonProjection::getLastName) - .as(StepVerifier::create) - .expectNext("Schnitzel") - .verifyComplete(); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQuery() { - neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1").all() - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQueryAndParam() { - neo4jTemplate.find(Person.class) - .matching("MATCH (p:Person {lastName: $lastName}) RETURN p", - Collections.singletonMap("lastName", "Schnitzel")) - .all() - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); - } - - @Test - void executableFindShouldWorkDomainObjectsWithQueryAndNullParams() { - - neo4jTemplate.find(Person.class) - .matching("MATCH (p:Person) RETURN p LIMIT 1", null) - .all() - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); - - } - - @Test - void oneShouldWork() { - neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p LIMIT 1").one() - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); - } - - @Test - void oneShouldWorkWithIncorrectResultSize() { - neo4jTemplate.find(Person.class).matching("MATCH (p:Person) RETURN p").one() - .as(StepVerifier::create) - .verifyError(IncorrectResultSizeDataAccessException.class); - } - - @Test - void statementShouldWork() { - Node person = Cypher.node("Person"); - Flux people = neo4jTemplate.find(Person.class).matching(Cypher.match(person) - .where(person.property("lastName").isEqualTo(Cypher.anonParameter("Siemons"))) - .returning(person).build()) - .all(); - people.map(Person::getLastName).as(StepVerifier::create).expectNext("Siemons").verifyComplete(); - } - - @Test - void statementWithParamsShouldWork() { - Node person = Cypher.node("Person"); - Flux people = neo4jTemplate.find(Person.class).matching(Cypher.match(person) - .where(person.property("lastName").isEqualTo(Cypher.parameter("lastName", "Siemons"))) - .returning(person).build(), Collections.singletonMap("lastName", "Schnitzel")) - .all(); - people.map(Person::getLastName).as(StepVerifier::create).expectNext("Schnitzel").verifyComplete(); - } - - @Test - // GH-2407 - void shouldSaveAllAsWithAssignedIdProjected() { - - neo4jTemplate.findById("x", PersonWithAssignedId.class) - .flatMapMany(p -> { - p.setLastName("modifiedLast"); - p.setFirstName("modifiedFirst"); - return neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjection.class); - }).map(ClosedProjection::getLastName) - .as(StepVerifier::create) - .expectNext("modifiedLast") - .verifyComplete(); - - neo4jTemplate.findById("x", PersonWithAssignedId.class) - .as(StepVerifier::create) - .consumeNextWith(p -> { - assertThat(p.getFirstName()).isEqualTo("John"); - assertThat(p.getLastName()).isEqualTo("modifiedLast"); - }) - .verifyComplete(); - } - - @Test - // GH-2407 - void shouldSaveAsWithAssignedIdProjected() { - - neo4jTemplate.findById("x", PersonWithAssignedId.class) - .flatMap(p -> { - p.setLastName("modifiedLast"); - p.setFirstName("modifiedFirst"); - return neo4jTemplate.saveAs(p, ClosedProjection.class); - }).map(ClosedProjection::getLastName) - .as(StepVerifier::create) - .expectNext("modifiedLast") - .verifyComplete(); - - neo4jTemplate.findById("x", PersonWithAssignedId.class) - .as(StepVerifier::create) - .consumeNextWith(p -> { - assertThat(p.getFirstName()).isEqualTo("John"); - assertThat(p.getLastName()).isEqualTo("modifiedLast"); - }) - .verifyComplete(); - } - - @Test - // GH-2415 - void saveWithProjectionImplementedByEntity(@Autowired Neo4jMappingContext mappingContext) { - - Neo4jPersistentEntity metaData = mappingContext.getPersistentEntity(BaseNodeEntity.class); - neo4jTemplate - .find(BaseNodeEntity.class) - .as(NodeEntity.class) - .matching(QueryFragmentsAndParameters.forCondition(metaData, Constants.NAME_OF_TYPED_ROOT_NODE.apply(metaData).property("nodeId").isEqualTo(Cypher.literalOf("root")))) - .one() - .flatMap(nodeEntity -> neo4jTemplate.saveAs(nodeEntity, NodeWithDefinedCredentials.class)) - .flatMap(nodeEntity -> neo4jTemplate.findById(nodeEntity.getNodeId(), NodeEntity.class)) - .as(StepVerifier::create) - .consumeNextWith(nodeEntity -> assertThat(nodeEntity.getChildren()).hasSize(1)) - .verifyComplete(); } @Configuration @@ -1027,30 +1077,36 @@ class ReactiveNeo4jTemplateIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } - @Override // needed here because there is no implicit registration of entities upfront some methods under test + @Override // needed here because there is no implicit registration of entities + // upfront some methods under test protected Collection getMappingBasePackages() { return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTransactionManagerTestIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTransactionManagerTestIT.java index a52440554..df2e13fb6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTransactionManagerTestIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jTransactionManagerTestIT.java @@ -15,35 +15,35 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -61,21 +61,21 @@ class ReactiveNeo4jTransactionManagerTestIT { } @Test // GH-2193 - void exceptionShouldNotBeShadowed( - @Autowired ReactiveNeo4jTransactionManager transactionManager, - @Autowired ReactiveNeo4jClient client, - @Autowired SomeRepository someRepository) { + void exceptionShouldNotBeShadowed(@Autowired ReactiveNeo4jTransactionManager transactionManager, + @Autowired ReactiveNeo4jClient client, @Autowired SomeRepository someRepository) { TransactionalOperator rxtx = TransactionalOperator.create(transactionManager); - rxtx.execute(txStatus -> client.query("CREATE (n:ShouldNotBeThere)").run() - .then(someRepository.broken().as(rxtx::transactional)) - ).then().as(StepVerifier::create).verifyError(InvalidDataAccessResourceUsageException.class); + rxtx.execute(txStatus -> client.query("CREATE (n:ShouldNotBeThere)") + .run() + .then(someRepository.broken().as(rxtx::transactional))) + .then() + .as(StepVerifier::create) + .verifyError(InvalidDataAccessResourceUsageException.class); try (Session session = neo4jConnectionSupport.getDriver().session()) { - long cnt = session - .executeRead(tx -> tx.run("MATCH (n:ShouldNotBeThere) RETURN count(n)").single().get(0)) - .asLong(); + long cnt = session.executeRead(tx -> tx.run("MATCH (n:ShouldNotBeThere) RETURN count(n)").single().get(0)) + .asLong(); assertThat(cnt).isEqualTo(0L); } } @@ -84,6 +84,7 @@ class ReactiveNeo4jTransactionManagerTestIT { @Query("Kaputt") Mono broken(); + } @Configuration @@ -92,26 +93,31 @@ class ReactiveNeo4jTransactionManagerTestIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveOptimisticLockingIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveOptimisticLockingIT.java index ae8d2680f..3fdd88664 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveOptimisticLockingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveOptimisticLockingIT.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -33,6 +27,9 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,9 +44,12 @@ import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepos import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier */ @@ -72,11 +72,11 @@ class ReactiveOptimisticLockingIT { @BeforeEach void setup() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -86,7 +86,8 @@ class ReactiveOptimisticLockingIT { // would love to verify version change null -> 0 and 0 -> 1 within one test VersionedThing thing1 = repository.save(new VersionedThing("Thing1")).block(); StepVerifier.create(repository.save(thing1)) - .assertNext(versionedThing -> assertThat(versionedThing.getMyVersion()).isEqualTo(1L)).verifyComplete(); + .assertNext(versionedThing -> assertThat(versionedThing.getMyVersion()).isEqualTo(1L)) + .verifyComplete(); } @@ -97,10 +98,12 @@ class ReactiveOptimisticLockingIT { VersionedThing thing2 = new VersionedThing("Thing2"); List thingsToSave = Arrays.asList(thing1, thing2); - StepVerifier.create(repository.saveAll(thingsToSave)).recordWith(ArrayList::new).expectNextCount(2) - .consumeRecordedWith(versionedThings -> assertThat(versionedThings) - .allMatch(versionedThing -> versionedThing.getMyVersion().equals(0L))) - .verifyComplete(); + StepVerifier.create(repository.saveAll(thingsToSave)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .consumeRecordedWith(versionedThings -> assertThat(versionedThings) + .allMatch(versionedThing -> versionedThing.getMyVersion().equals(0L))) + .verifyComplete(); } @@ -113,9 +116,9 @@ class ReactiveOptimisticLockingIT { parentThing.setOtherVersionedThings(Collections.singletonList(childThing)); StepVerifier.create(repository.save(parentThing)) - .assertNext( - versionedThing -> assertThat(versionedThing.getOtherVersionedThings().get(0).getMyVersion()).isEqualTo(0L)) - .verifyComplete(); + .assertNext(versionedThing -> assertThat(versionedThing.getOtherVersionedThings().get(0).getMyVersion()) + .isEqualTo(0L)) + .verifyComplete(); } @Test @@ -141,16 +144,19 @@ class ReactiveOptimisticLockingIT { savedThings.get(1).setMyVersion(1L); // Version in DB is 0 StepVerifier.create(repository.saveAll(savedThings)) - .expectNextCount(1L) - .expectError(OptimisticLockingFailureException.class) - .verify(); + .expectNextCount(1L) + .expectError(OptimisticLockingFailureException.class) + .verify(); - // Make sure the first object that has the correct version number doesn't get persisted either - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long cnt = session.run( - "MATCH (n:VersionedThing) WHERE id(n) = $id AND n.mutableProperty = 'changed' RETURN count(*)", - Collections.singletonMap("id", savedThings.get(0).getId()) - ).single().get(0).asLong(); + // Make sure the first object that has the correct version number doesn't get + // persisted either + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long cnt = session + .run("MATCH (n:VersionedThing) WHERE id(n) = $id AND n.mutableProperty = 'changed' RETURN count(*)", + Collections.singletonMap("id", savedThings.get(0).getId())) + .single() + .get(0) + .asLong(); assertThat(cnt).isEqualTo(0L); } } @@ -162,7 +168,8 @@ class ReactiveOptimisticLockingIT { VersionedThing childThing = new VersionedThing("Thing2"); thing.setOtherVersionedThings(Collections.singletonList(childThing)); VersionedThing savedThing = repository.save(thing).block(); - savedThing.getOtherVersionedThings().get(0).setMyVersion(1L); // Version in DB is 0 + savedThing.getOtherVersionedThings().get(0).setMyVersion(1L); // Version in DB is + // 0 StepVerifier.create(repository.save(savedThing)).expectError(OptimisticLockingFailureException.class).verify(); @@ -177,7 +184,8 @@ class ReactiveOptimisticLockingIT { assertThat(thing.getMyVersion()).isEqualTo(0L); StepVerifier.create(repository.save(thing)) - .assertNext(savedThing -> assertThat(savedThing.getMyVersion()).isEqualTo(1L)).verifyComplete(); + .assertNext(savedThing -> assertThat(savedThing.getMyVersion()).isEqualTo(1L)) + .verifyComplete(); } @@ -191,10 +199,12 @@ class ReactiveOptimisticLockingIT { List versionedThings = repository.saveAll(thingsToSave).collectList().block(); - StepVerifier.create(repository.saveAll(versionedThings)).recordWith(ArrayList::new).expectNextCount(2) - .consumeRecordedWith( - savedThings -> assertThat(savedThings).allMatch(versionedThing -> versionedThing.getMyVersion().equals(1L))) - .verifyComplete(); + StepVerifier.create(repository.saveAll(versionedThings)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .consumeRecordedWith(savedThings -> assertThat(savedThings) + .allMatch(versionedThing -> versionedThing.getMyVersion().equals(1L))) + .verifyComplete(); } @Test @@ -221,24 +231,26 @@ class ReactiveOptimisticLockingIT { versionedThings.get(0).setMyVersion(1L); // Version in DB is 0 - StepVerifier.create(repository.saveAll(versionedThings)).expectError(OptimisticLockingFailureException.class) - .verify(); + StepVerifier.create(repository.saveAll(versionedThings)) + .expectError(OptimisticLockingFailureException.class) + .verify(); } @Test void shouldNotFailOnDeleteByIdWithNullVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - StepVerifier.create(repository.deleteById(1L)) - .verifyComplete(); + StepVerifier.create(repository.deleteById(1L)).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount").single() - .get("vCount").asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount") + .single() + .get("vCount") + .asLong(); assertThat(count).isEqualTo(0); } @@ -246,30 +258,31 @@ class ReactiveOptimisticLockingIT { @Test void shouldNotFailOnDeleteByEntityWithNullVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } StepVerifier.create(repository.findById(1L).map(thing -> repository.deleteById(1L))) - .expectNextCount(1L) - .verifyComplete(); + .expectNextCount(1L) + .verifyComplete(); } @Test void shouldNotFailOnDeleteByIdWithAnyVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1, myVersion:3})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - StepVerifier.create(repository.deleteById(1L)) - .verifyComplete(); + StepVerifier.create(repository.deleteById(1L)).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount").single() - .get("vCount").asLong(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long count = session.run("MATCH (v:VersionedThingWithAssignedId) return count(v) as vCount") + .single() + .get("vCount") + .asLong(); assertThat(count).isEqualTo(0); } @@ -277,15 +290,14 @@ class ReactiveOptimisticLockingIT { @Test void shouldFailOnDeleteByEntityWithWrongVersion(@Autowired VersionedThingWithAssignedIdRepository repository) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { session.run("CREATE (v:VersionedThingWithAssignedId {id:1, myVersion:2})").consume(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } - StepVerifier.create(repository.findById(1L) - .flatMap(thing -> { - thing.setMyVersion(3L); - return repository.delete(thing); + StepVerifier.create(repository.findById(1L).flatMap(thing -> { + thing.setMyVersion(3L); + return repository.delete(thing); })).verifyError(OptimisticLockingFailureException.class); } @@ -297,27 +309,20 @@ class ReactiveOptimisticLockingIT { VersionedThing thing2 = new VersionedThing("Thing2"); thing1.setOtherVersionedThings(Collections.singletonList(thing2)); - repository.save(thing1) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + repository.save(thing1).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - Flux.zip(repository.findById(thing1.getId()), (repository.findById(thing2.getId()))) - .flatMap(t -> { - VersionedThing thing1n = t.getT1(); - VersionedThing thing2n = t.getT2(); + Flux.zip(repository.findById(thing1.getId()), (repository.findById(thing2.getId()))).flatMap(t -> { + VersionedThing thing1n = t.getT1(); + VersionedThing thing2n = t.getT2(); - thing2n.setOtherVersionedThings(Collections.singletonList(thing1n)); - return repository.save(thing2n); - }) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + thing2n.setOtherVersionedThings(Collections.singletonList(thing1n)); + return repository.save(thing2n); + }).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { List result = session - .run("MATCH (t:VersionedThing{name:'Thing1'})-[:HAS]->(:VersionedThing{name:'Thing2'}) return t") - .list(); + .run("MATCH (t:VersionedThing{name:'Thing1'})-[:HAS]->(:VersionedThing{name:'Thing2'}) return t") + .list(); assertThat(result).hasSize(1); } } @@ -335,31 +340,31 @@ class ReactiveOptimisticLockingIT { thing1Relationships.add(thing3); thing1Relationships.add(thing4); thing1.setOtherVersionedThings(thing1Relationships); - StepVerifier.create(repository.save(thing1)) - .expectNextCount(1) - .verifyComplete(); + StepVerifier.create(repository.save(thing1)).expectNextCount(1).verifyComplete(); - Flux.zip(repository.findById(thing1.getId()), repository.findById(thing3.getId())) - .flatMap(tuple -> { - tuple.getT2().setOtherVersionedThings(Collections.singletonList(tuple.getT1())); - return repository.save(tuple.getT2()); - }) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + Flux.zip(repository.findById(thing1.getId()), repository.findById(thing3.getId())).flatMap(tuple -> { + tuple.getT2().setOtherVersionedThings(Collections.singletonList(tuple.getT1())); + return repository.save(tuple.getT2()); + }).as(StepVerifier::create).expectNextCount(1).verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Long relationshipCount = session - .run("MATCH (:VersionedThing)-[r:HAS]->(:VersionedThing) return count(r) as relationshipCount") - .single().get("relationshipCount").asLong(); + .run("MATCH (:VersionedThing)-[r:HAS]->(:VersionedThing) return count(r) as relationshipCount") + .single() + .get("relationshipCount") + .asLong(); assertThat(relationshipCount).isEqualTo(4); } } - interface VersionedThingRepository extends ReactiveNeo4jRepository {} + interface VersionedThingRepository extends ReactiveNeo4jRepository { + + } interface VersionedThingWithAssignedIdRepository - extends ReactiveNeo4jRepository {} + extends ReactiveNeo4jRepository { + + } @Configuration @EnableTransactionManagement @@ -367,25 +372,30 @@ class ReactiveOptimisticLockingIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveProjectionIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveProjectionIT.java index ba6389e8f..cc23008d5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveProjectionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveProjectionIT.java @@ -15,13 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.Collections; import java.util.List; @@ -36,6 +29,10 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.types.MapAccessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -59,10 +56,13 @@ import org.springframework.data.neo4j.repository.support.ReactiveCypherdslStatem import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.repository.query.Param; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gerrit Meier */ @@ -71,14 +71,19 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; class ReactiveProjectionIT { private static final String FIRST_NAME = "Hans"; + private static final String LAST_NAME = "Mueller"; + private static final String CITY = "Braunschweig"; private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; + private Long projectionTestRootId; + private Long projectionTest1O1Id; + private Long projectionTestLevel1Id; @Autowired @@ -86,15 +91,25 @@ class ReactiveProjectionIT { this.driver = driver; } + private static Statement whoHasFirstName(String firstName) { + Node p = Cypher.node("Person").named("p"); + return Cypher.match(p) + .where(p.property("firstName").isEqualTo(Cypher.anonParameter(firstName))) + .returning(p.getRequiredSymbolicName()) + .build(); + } + @BeforeEach void setup(@Autowired BookmarkCapture bookmarkCapture) { - Session session = driver.session(bookmarkCapture.createSessionConfig()); + Session session = this.driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction(); transaction.run("MATCH (n) detach delete n"); - transaction.run("CREATE (:Person{firstName:'%s', lastName:'%s'})-[:LIVES_AT]->(:Address{city:'%s'})" .formatted(FIRST_NAME, LAST_NAME, CITY)); - transaction.run("CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p"); + transaction.run("CREATE (:Person{firstName:'%s', lastName:'%s'})-[:LIVES_AT]->(:Address{city:'%s'})" + .formatted(FIRST_NAME, LAST_NAME, CITY)); + transaction.run( + "CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p"); Record result = transaction.run(""" create (r:ProjectionTestRoot {name: 'root'}) @@ -113,9 +128,9 @@ class ReactiveProjectionIT { return id(r), id(l11), id(o) """).single(); - projectionTestRootId = result.get(0).asLong(); - projectionTestLevel1Id = result.get(1).asLong(); - projectionTest1O1Id = result.get(2).asLong(); + this.projectionTestRootId = result.get(0).asLong(); + this.projectionTestLevel1Id = result.get(1).asLong(); + this.projectionTest1O1Id = result.get(2).asLong(); transaction.commit(); transaction.close(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -160,152 +175,140 @@ class ReactiveProjectionIT { void findDynamicProjectionForNamesOnly(@Autowired ReactiveProjectionPersonRepository repository) { StepVerifier.create(repository.findByLastNameAndFirstName(LAST_NAME, FIRST_NAME, NamesOnly.class)) - .assertNext(person -> { - assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); - assertThat(person.getLastName()).isEqualTo(LAST_NAME); + .assertNext(person -> { + assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); + assertThat(person.getLastName()).isEqualTo(LAST_NAME); - String expectedFullName = FIRST_NAME + " " + LAST_NAME; - assertThat(person.getFullName()).isEqualTo(expectedFullName); - }).verifyComplete(); + String expectedFullName = FIRST_NAME + " " + LAST_NAME; + assertThat(person.getFullName()).isEqualTo(expectedFullName); + }) + .verifyComplete(); } @Test void findDynamicProjectionForPersonSummary(@Autowired ReactiveProjectionPersonRepository repository) { StepVerifier.create(repository.findByLastNameAndFirstName(LAST_NAME, FIRST_NAME, PersonSummary.class)) - .assertNext(person -> { - assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); - assertThat(person.getLastName()).isEqualTo(LAST_NAME); - assertThat(person.getAddress()).isNotNull(); + .assertNext(person -> { + assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); + assertThat(person.getLastName()).isEqualTo(LAST_NAME); + assertThat(person.getAddress()).isNotNull(); - PersonSummary.AddressSummary address = person.getAddress(); - assertThat(address.getCity()).isEqualTo(CITY); - }).verifyComplete(); + PersonSummary.AddressSummary address = person.getAddress(); + assertThat(address.getCity()).isEqualTo(CITY); + }) + .verifyComplete(); } @Test void findDynamicProjectionForNamesOnlyDto(@Autowired ReactiveProjectionPersonRepository repository) { StepVerifier.create(repository.findByLastNameAndFirstName(LAST_NAME, FIRST_NAME, NamesOnlyDto.class)) - .assertNext(person -> { - assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); - assertThat(person.getLastName()).isEqualTo(LAST_NAME); - }).verifyComplete(); + .assertNext(person -> { + assertThat(person.getFirstName()).isEqualTo(FIRST_NAME); + assertThat(person.getLastName()).isEqualTo(LAST_NAME); + }) + .verifyComplete(); } @Test void findStringBasedClosedProjection(@Autowired ReactiveProjectionPersonRepository repository) { - StepVerifier.create(repository.customQueryByFirstName(FIRST_NAME)) - .assertNext(personSummary -> { - assertThat(personSummary).isNotNull(); - assertThat(personSummary.getFirstName()).isEqualTo(FIRST_NAME); - assertThat(personSummary.getLastName()).isEqualTo(LAST_NAME); - }) - .verifyComplete(); + StepVerifier.create(repository.customQueryByFirstName(FIRST_NAME)).assertNext(personSummary -> { + assertThat(personSummary).isNotNull(); + assertThat(personSummary.getFirstName()).isEqualTo(FIRST_NAME); + assertThat(personSummary.getLastName()).isEqualTo(LAST_NAME); + }).verifyComplete(); } @Test void findCypherDSLClosedProjection(@Autowired ReactiveProjectionPersonRepository repository) { StepVerifier.create(repository.findOne(whoHasFirstName(FIRST_NAME), PersonSummary.class)) - .assertNext(personSummary -> { - assertThat(personSummary).isNotNull(); - assertThat(personSummary.getFirstName()).isEqualTo(FIRST_NAME); - assertThat(personSummary.getLastName()).isEqualTo(LAST_NAME); - }) - .verifyComplete(); - } - - private static Statement whoHasFirstName(String firstName) { - Node p = Cypher.node("Person").named("p"); - return Cypher.match(p) - .where(p.property("firstName").isEqualTo(Cypher.anonParameter(firstName))) - .returning( - p.getRequiredSymbolicName() - ) - .build(); + .assertNext(personSummary -> { + assertThat(personSummary).isNotNull(); + assertThat(personSummary.getFirstName()).isEqualTo(FIRST_NAME); + assertThat(personSummary.getLastName()).isEqualTo(LAST_NAME); + }) + .verifyComplete(); } @Test // GH-2164 void findByIdWithProjectionShouldWork(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findById(projectionTestRootId, SimpleProjection.class)) - .assertNext(projection -> { - assertThat(projection.getName()).isEqualTo("root"); - }) - .verifyComplete(); + StepVerifier.create(repository.findById(this.projectionTestRootId, SimpleProjection.class)) + .assertNext(projection -> { + assertThat(projection.getName()).isEqualTo("root"); + }) + .verifyComplete(); } @Test // GH-2165 void relationshipsShouldBeIncludedInProjections(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findById(projectionTestRootId, SimpleProjectionWithLevelAndLower.class)) - .assertNext(projection -> - assertThat(projection).satisfies(p -> { - assertThat(p.getName()).isEqualTo("root"); - assertThat(p.getOneOone()).extracting(ProjectionTest1O1::getName).isEqualTo("1o1"); - assertThat(p.getLevel1()).hasSize(2); - assertThat(p.getLevel1().stream()) - .anyMatch(e -> e.getId().equals(projectionTestLevel1Id) && e.getLevel2().size() == 2); - })) - .verifyComplete(); + StepVerifier.create(repository.findById(this.projectionTestRootId, SimpleProjectionWithLevelAndLower.class)) + .assertNext(projection -> assertThat(projection).satisfies(p -> { + assertThat(p.getName()).isEqualTo("root"); + assertThat(p.getOneOone()).extracting(ProjectionTest1O1::getName).isEqualTo("1o1"); + assertThat(p.getLevel1()).hasSize(2); + assertThat(p.getLevel1().stream()) + .anyMatch(e -> e.getId().equals(this.projectionTestLevel1Id) && e.getLevel2().size() == 2); + })) + .verifyComplete(); } @Test // GH-2165 void nested1to1ProjectionsShouldWork(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findById(projectionTestRootId, ProjectedOneToOne.class)) - .assertNext(projection -> - assertThat(projection).satisfies(p -> { - assertThat(p.getName()).isEqualTo("root"); - assertThat(p.getOneOone()).extracting(ProjectedOneToOne.Subprojection::getFullName) - .isEqualTo(projectionTest1O1Id + " 1o1"); - })) - .verifyComplete(); + StepVerifier.create(repository.findById(this.projectionTestRootId, ProjectedOneToOne.class)) + .assertNext(projection -> assertThat(projection).satisfies(p -> { + assertThat(p.getName()).isEqualTo("root"); + assertThat(p.getOneOone()).extracting(ProjectedOneToOne.Subprojection::getFullName) + .isEqualTo(this.projectionTest1O1Id + " 1o1"); + })) + .verifyComplete(); } @Test void nested1to1ProjectionsWithNestedProjectionShouldWork(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findById(projectionTestRootId, ProjectionWithNestedProjection.class)) - .assertNext(projection -> - assertThat(projection).satisfies(p -> { - assertThat(p.getName()).isEqualTo("root"); - assertThat(p.getLevel1()).extracting("name").containsExactlyInAnyOrder("level11", "level12"); - assertThat(p.getLevel1()).flatExtracting("level2").extracting("name") - .containsExactlyInAnyOrder("level21", "level22", "level23"); - })) - .verifyComplete(); + StepVerifier.create(repository.findById(this.projectionTestRootId, ProjectionWithNestedProjection.class)) + .assertNext(projection -> assertThat(projection).satisfies(p -> { + assertThat(p.getName()).isEqualTo("root"); + assertThat(p.getLevel1()).extracting("name").containsExactlyInAnyOrder("level11", "level12"); + assertThat(p.getLevel1()).flatExtracting("level2") + .extracting("name") + .containsExactlyInAnyOrder("level21", "level22", "level23"); + })) + .verifyComplete(); } @Test // GH-2165 void nested1toManyProjectionsShouldWork(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findById(projectionTestRootId, ProjectedOneToMany.class)) - .assertNext(projection -> - assertThat(projection).satisfies(p -> { - assertThat(p.getName()).isEqualTo("root"); - assertThat(p.getLevel1()).hasSize(2); - })) - .verifyComplete(); + StepVerifier.create(repository.findById(this.projectionTestRootId, ProjectedOneToMany.class)) + .assertNext(projection -> assertThat(projection).satisfies(p -> { + assertThat(p.getName()).isEqualTo("root"); + assertThat(p.getLevel1()).hasSize(2); + })) + .verifyComplete(); } @Test // GH-2164 void findByIdInDerivedFinderMethodInRelatedObjectShouldWork(@Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findOneByLevel1Id(projectionTestLevel1Id)) - .assertNext(projection -> assertThat(projection.getName()).isEqualTo("root")) - .verifyComplete(); + StepVerifier.create(repository.findOneByLevel1Id(this.projectionTestLevel1Id)) + .assertNext(projection -> assertThat(projection.getName()).isEqualTo("root")) + .verifyComplete(); } @Test // GH-2164 void findByIdInDerivedFinderMethodInRelatedObjectWithProjectionShouldWork( @Autowired TreestructureRepository repository) { - StepVerifier.create(repository.findOneByLevel1Id(projectionTestLevel1Id, SimpleProjection.class)) - .assertNext(projection -> assertThat(projection.getName()).isEqualTo("root")) - .verifyComplete(); + StepVerifier.create(repository.findOneByLevel1Id(this.projectionTestLevel1Id, SimpleProjection.class)) + .assertNext(projection -> assertThat(projection.getName()).isEqualTo("root")) + .verifyComplete(); } @Test // GH-2371 @@ -313,35 +316,31 @@ class ReactiveProjectionIT { repository.findAll().as(StepVerifier::create).expectNextCount(1L); - repository.findByName("meistermeier") - .as(StepVerifier::create) - .assertNext(person -> { - assertThat(person.getFirstName()).isEqualTo("Gerrit"); - assertThat(person.getMittlererName()).isEqualTo("unknown"); - }) - .verifyComplete(); + repository.findByName("meistermeier").as(StepVerifier::create).assertNext(person -> { + assertThat(person.getFirstName()).isEqualTo("Gerrit"); + assertThat(person.getMittlererName()).isEqualTo("unknown"); + }).verifyComplete(); } @Test // GH-2371 - void saveWithCustomPropertyNameWorks(@Autowired BookmarkCapture bookmarkCapture, @Autowired ReactiveNeo4jTemplate neo4jTemplate) { + void saveWithCustomPropertyNameWorks(@Autowired BookmarkCapture bookmarkCapture, + @Autowired ReactiveNeo4jTemplate neo4jTemplate) { neo4jTemplate - .findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(), PersonWithNoConstructor.class) - .doOnNext(person -> { - person.setName("rotnroll666"); - person.setFirstName("Michael"); - person.setMiddleName("foo"); - }).flatMap(p -> neo4jTemplate.saveAs(p, ProjectedPersonWithNoConstructor.class)) - .as(StepVerifier::create) - .expectNextCount(1L) - .verifyComplete(); + .findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(), + PersonWithNoConstructor.class) + .doOnNext(person -> { + person.setName("rotnroll666"); + person.setFirstName("Michael"); + person.setMiddleName("foo"); + }) + .flatMap(p -> neo4jTemplate.saveAs(p, ProjectedPersonWithNoConstructor.class)) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); - - - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Record record = session - .run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p") - .single(); + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { + Record record = session.run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p").single(); MapAccessor p = record.get("p").asNode(); assertThat(p.get("first_name").asString()).isEqualTo("Michael"); @@ -356,16 +355,17 @@ class ReactiveProjectionIT { String getFirstName(); String getMittlererName(); + } interface PersonWithNoConstructorRepository extends ReactiveNeo4jRepository { Mono findByName(String name); + } - - interface ReactiveProjectionPersonRepository extends ReactiveNeo4jRepository, - ReactiveCypherdslStatementExecutor { + interface ReactiveProjectionPersonRepository + extends ReactiveNeo4jRepository, ReactiveCypherdslStatementExecutor { Flux findByLastName(String lastName); @@ -377,6 +377,7 @@ class ReactiveProjectionIT { @Query("MATCH (n:Person) where n.firstName = $firstName return n") Mono customQueryByFirstName(@Param("firstName") String firstName); + } interface TreestructureRepository extends ReactiveNeo4jRepository { @@ -386,11 +387,13 @@ class ReactiveProjectionIT { Mono findOneByLevel1Id(Long idOfLevel1); Mono findOneByLevel1Id(Long idOfLevel1, Class typeOfProjection); + } interface SimpleProjection { String getName(); + } interface SimpleProjectionWithLevelAndLower { @@ -400,6 +403,7 @@ class ReactiveProjectionIT { ProjectionTest1O1 getOneOone(); List getLevel1(); + } interface ProjectedOneToOne { @@ -411,11 +415,14 @@ class ReactiveProjectionIT { interface Subprojection { /** - * @return Some arbitrary computed projection result to make sure that machinery works as well + * @return Some arbitrary computed projection result to make sure that + * machinery works as well */ @Value("#{target.id + ' ' + target.name}") String getFullName(); + } + } interface ProjectedOneToMany { @@ -427,11 +434,14 @@ class ReactiveProjectionIT { interface Subprojection { /** - * @return Some arbitrary computed projection result to make sure that machinery works as well + * @return Some arbitrary computed projection result to make sure that + * machinery works as well */ @Value("#{target.id + ' ' + target.name}") String getFullName(); + } + } interface ProjectionWithNestedProjection { @@ -441,13 +451,19 @@ class ReactiveProjectionIT { List getLevel1(); interface Subprojection1 { + String getName(); + List getLevel2(); + } interface Subprojection2 { + String getName(); + } + } @Configuration @@ -456,26 +472,30 @@ class ReactiveProjectionIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java index f1ed0f8df..d79310729 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java @@ -15,22 +15,25 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Tag; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.test.StepVerifier; - +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.Expressions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -41,16 +44,12 @@ import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepos import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import com.querydsl.core.types.Ops; -import com.querydsl.core.types.Order; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.Expressions; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -61,26 +60,27 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Path personPath; + private final Path firstNamePath; + private final Path lastNamePath; ReactiveQuerydslNeo4jPredicateExecutorIT() { this.personPath = Expressions.path(Person.class, "person"); - this.firstNamePath = Expressions.path(String.class, personPath, "firstName"); - this.lastNamePath = Expressions.path(String.class, personPath, "lastName"); + this.firstNamePath = Expressions.path(String.class, this.personPath, "firstName"); + this.lastNamePath = Expressions.path(String.class, this.personPath, "lastName"); } @BeforeAll protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction() - ) { + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); - transaction - .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run( + "CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -90,42 +90,267 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT { @Test // GH-2361 void fluentFindOneShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); repository.findBy(predicate, q -> q.one()) - .map(Person::getLastName) - .as(StepVerifier::create) - .expectNext("Schneider") - .verifyComplete(); + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); } @Test // GH-2361 void fluentFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); repository.findBy(predicate, q -> q.all()) - .map(Person::getFirstName) - .sort() // Due to not having something like containsExactlyInAnyOrder - .as(StepVerifier::create) - .expectNext("Bela", "Helge") - .verifyComplete(); + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); } @Test // GH-2361 void fluentFindAllProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); repository.findBy(predicate, q -> q.project("firstName").all()) - .as(StepVerifier::create) - .expectNextMatches(p -> { - assertThat(p.getFirstName()).isEqualTo("Helge"); - assertThat(p.getId()).isNotNull(); + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getFirstName()).isEqualTo("Helge"); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getLastName()).isNull(); + assertThat(p.getAddress()).isNull(); + return true; + }) + .verifyComplete(); + } + + @Test // GH-2361 + void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()) + .map(DtoPersonProjection::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).first()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).all()) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider", "LB", "LA", "B.") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))) + .as(StepVerifier::create) + .expectNextMatches(people -> { + + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); + assertThat(people.hasPrevious()).isTrue(); + assertThat(people.hasNext()).isFalse(); + return true; + }) + .verifyComplete(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + + repository + .findBy(predicate, + q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset())) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Helge"); + + assertThat(peopleWindow.isLast()).isFalse(); + assertThat(peopleWindow.hasNext()).isTrue(); + + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))) + .isEqualTo(ScrollPosition.offset(0)); + return true; + }) + .verifyComplete(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + + repository + .findBy(predicate, + q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(peopleWindow.isLast()).isTrue(); + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))) + .isEqualTo(ScrollPosition.offset(1)); + return true; + }) + .verifyComplete(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + + repository + .findBy(predicate, + q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0)); + repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)) + .as(StepVerifier::create) + .expectNextMatches(nextPeopleWindow -> { + + assertThat(nextPeopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(nextPeopleWindow.isLast()).isTrue(); + return true; + }); + return true; + }); + + } + + @Test // GH-2361 + void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.exists()).as(StepVerifier::create).expectNext(true).verifyComplete(); + } + + @Test // GH-2361 + void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.count()).as(StepVerifier::create).expectNext(2L).verifyComplete(); + } + + @Test // GH-2361 + void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findOne(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge"))) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); + } + + @Test // GH-2361 + void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository + .findAll(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B.")))) + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository + .findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + new OrderSpecifier(Order.DESC, this.lastNamePath)) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository + .findAll( + Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B."))), + Sort.by("lastName").descending()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(new OrderSpecifier(Order.DESC, this.lastNamePath)) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "B", "A", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void countShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository + .count(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, this.lastNamePath, Expressions.asString("B.")))) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + + @Test // GH-2361 + void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.exists(Expressions.predicate(Ops.EQ, this.firstNamePath, Expressions.asString("A"))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + interface QueryDSLPersonRepository + extends ReactiveNeo4jRepository, ReactiveQuerydslPredicateExecutor { - assertThat(p.getLastName()).isNull(); - assertThat(p.getAddress()).isNull(); - return true; - }) - .verifyComplete(); } static class DtoPersonProjection { @@ -136,217 +361,10 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT { this.firstName = firstName; } - public String getFirstName() { - return firstName; + String getFirstName() { + return this.firstName; } - } - @Test // GH-2361 - void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); - repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()) - .map(DtoPersonProjection::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge") - .verifyComplete(); - } - - @Test // GH-2361 - void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.TRUE.isTrue(); - repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).first()) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge") - .verifyComplete(); - } - - @Test // GH-2361 - void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.TRUE.isTrue(); - repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).all()) - .map(Person::getLastName) - .as(StepVerifier::create) - .expectNext("Schneider", "LB", "LA", "B.") - .verifyComplete(); - } - - @Test // GH-2361 - void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - repository.findBy(predicate, q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))) - .as(StepVerifier::create) - .expectNextMatches(people -> { - - assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); - assertThat(people.hasPrevious()).isTrue(); - assertThat(people.hasNext()).isFalse(); - return true; - }).verifyComplete(); - } - - @Test - @Tag("GH-2726") - void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - - repository.findBy(predicate, q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset())) - .as(StepVerifier::create) - .expectNextMatches(peopleWindow -> { - - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Helge"); - - assertThat(peopleWindow.isLast()).isFalse(); - assertThat(peopleWindow.hasNext()).isTrue(); - - assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(0)); - return true; - }).verifyComplete(); - } - - @Test - @Tag("GH-2726") - void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - - repository.findBy(predicate, q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))) - .as(StepVerifier::create) - .expectNextMatches(peopleWindow -> { - assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela"); - - assertThat(peopleWindow.isLast()).isTrue(); - assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(1)); - return true; - }).verifyComplete(); - } - - @Test - @Tag("GH-2726") - void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) { - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - - repository.findBy(predicate, q -> q.limit(1).sortBy(Sort.by("firstName").descending()).scroll(ScrollPosition.offset(0))) - .as(StepVerifier::create) - .expectNextMatches(peopleWindow -> { - ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0)); - repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)) - .as(StepVerifier::create) - .expectNextMatches(nextPeopleWindow -> { - - assertThat(nextPeopleWindow.getContent()).extracting(Person::getFirstName) - .containsExactlyInAnyOrder("Bela"); - - assertThat(nextPeopleWindow.isLast()).isTrue(); - return true; - }); - return true; - }); - - } - - @Test // GH-2361 - void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); - repository.findBy(predicate, q -> q.exists()).as(StepVerifier::create).expectNext(true).verifyComplete(); - } - - @Test // GH-2361 - void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { - - Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); - repository.findBy(predicate, q -> q.count()).as(StepVerifier::create).expectNext(2L).verifyComplete(); - } - - @Test // GH-2361 - void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.findOne(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))) - .map(Person::getLastName) - .as(StepVerifier::create) - .expectNext("Schneider") - .verifyComplete(); - } - - @Test // GH-2361 - void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")))) - .map(Person::getFirstName) - .sort() // Due to not having something like containsExactlyInAnyOrder - .as(StepVerifier::create) - .expectNext("Bela", "Helge") - .verifyComplete(); - } - - @Test // GH-2361 - void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - new OrderSpecifier(Order.DESC, lastNamePath) - ) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "Bela") - .verifyComplete(); - } - - @Test // GH-2361 - void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), - Sort.by("lastName").descending() - ) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "Bela") - .verifyComplete(); - } - - @Test // GH-2361 - void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.findAll(new OrderSpecifier(Order.DESC, lastNamePath)) - .map(Person::getFirstName) - .as(StepVerifier::create) - .expectNext("Helge", "B", "A", "Bela") - .verifyComplete(); - } - - @Test // GH-2361 - void countShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.count(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))) - ).as(StepVerifier::create) - .expectNext(2L) - .verifyComplete(); - } - - @Test // GH-2361 - void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { - - repository.exists(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("A"))) - .as(StepVerifier::create) - .expectNext(true) - .verifyComplete(); - } - - interface QueryDSLPersonRepository extends ReactiveNeo4jRepository, ReactiveQuerydslPredicateExecutor { } @Configuration @@ -355,26 +373,31 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRelationshipsIT.java index 70f45a139..6b535896f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRelationshipsIT.java @@ -15,16 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; - import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -34,18 +24,29 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.integration.shared.common.MultipleRelationshipsThing; import org.springframework.data.neo4j.integration.shared.common.RelationshipsITBase; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** - * Test cases for various relationship scenarios (self references, multiple times to same instance). + * Test cases for various relationship scenarios (self references, multiple times to same + * instance). * * @author Michael J. Simons */ @@ -59,89 +60,99 @@ class ReactiveRelationshipsIT extends RelationshipsITBase { @Test void shouldSaveSingleRelationship(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); p.setTypeA(new MultipleRelationshipsThing("c")); - repository.save(p).map(MultipleRelationshipsThing::getId).flatMap(repository::findById).as(StepVerifier::create) - .assertNext(loadedThing -> assertThat(loadedThing).extracting(MultipleRelationshipsThing::getTypeA) - .extracting(MultipleRelationshipsThing::getName).isEqualTo("c")) - .verifyComplete(); + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> assertThat(loadedThing).extracting(MultipleRelationshipsThing::getTypeA) + .extracting(MultipleRelationshipsThing::getName) + .isEqualTo("c")) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") - .list(r -> r.get("name").asString()); + .list(r -> r.get("name").asString()); assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); } } @Test void shouldSaveSingleRelationshipInList(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c"))); - repository.save(p).map(MultipleRelationshipsThing::getId).flatMap(repository::findById).as(StepVerifier::create) - .assertNext(loadedThing -> assertThat(loadedThing.getTypeB()).extracting(MultipleRelationshipsThing::getName) + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext( + loadedThing -> assertThat(loadedThing.getTypeB()).extracting(MultipleRelationshipsThing::getName) .containsExactly("c")) - .verifyComplete(); + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") - .list(r -> r.get("name").asString()); + .list(r -> r.get("name").asString()); assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); } } /** * This stores multiple, different instances. - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameObjectType(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); p.setTypeA(new MultipleRelationshipsThing("c1")); p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c2"))); p.setTypeC(Collections.singletonList(new MultipleRelationshipsThing("c3"))); - repository.save(p).map(MultipleRelationshipsThing::getId).flatMap(repository::findById).as(StepVerifier::create) - .assertNext(loadedThing -> { - MultipleRelationshipsThing typeA = loadedThing.getTypeA(); - List typeB = loadedThing.getTypeB(); - List typeC = loadedThing.getTypeC(); + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); - assertThat(typeA).isNotNull(); - assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); - assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c2"); - assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c3"); - }).verifyComplete(); + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c2"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c3"); + }) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session - .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") - .list(record -> { - String type = record.get("r").asRelationship().type(); - String name = record.get("o").get("name").asString(); - return type + "_" + name; - }); + .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c2", "TYPE_C_c3"); } } /** * This stores the same instance in different relationships - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameInstance(@Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); @@ -149,41 +160,43 @@ class ReactiveRelationshipsIT extends RelationshipsITBase { p.setTypeB(Collections.singletonList(c)); p.setTypeC(Collections.singletonList(c)); - repository.save(p).map(MultipleRelationshipsThing::getId).flatMap(repository::findById).as(StepVerifier::create) - .assertNext(loadedThing -> { + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { - MultipleRelationshipsThing typeA = loadedThing.getTypeA(); - List typeB = loadedThing.getTypeB(); - List typeC = loadedThing.getTypeC(); + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); - assertThat(typeA).isNotNull(); - assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); - assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); - assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); - }).verifyComplete(); + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { List names = session - .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") - .list(record -> { - String type = record.get("r").asRelationship().type(); - String name = record.get("o").get("name").asString(); - return type + "_" + name; - }); + .run("MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); } } /** * This stores the same instance in different relationships - * * @param repository The repository to use. */ @Test void shouldSaveMultipleRelationshipsOfSameInstanceWithBackReference( - @Autowired MultipleRelationshipsThingRepository repository, - @Autowired BookmarkCapture bookmarkCapture) { + @Autowired MultipleRelationshipsThingRepository repository, @Autowired BookmarkCapture bookmarkCapture) { MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); @@ -193,20 +206,24 @@ class ReactiveRelationshipsIT extends RelationshipsITBase { c.setTypeA(p); - repository.save(p).map(MultipleRelationshipsThing::getId).flatMap(repository::findById).as(StepVerifier::create) - .assertNext(loadedThing -> { + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { - MultipleRelationshipsThing typeA = loadedThing.getTypeA(); - List typeB = loadedThing.getTypeB(); - List typeC = loadedThing.getTypeC(); + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); - assertThat(typeA).isNotNull(); - assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); - assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); - assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); - }).verifyComplete(); + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }) + .verifyComplete(); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(bookmarkCapture.createSessionConfig())) { Function withMapper = record -> { String type = record.get("r").asRelationship().type(); @@ -223,7 +240,9 @@ class ReactiveRelationshipsIT extends RelationshipsITBase { } } - interface MultipleRelationshipsThingRepository extends ReactiveCrudRepository {} + interface MultipleRelationshipsThingRepository extends ReactiveCrudRepository { + + } @Configuration @EnableTransactionManagement @@ -231,25 +250,30 @@ class ReactiveRelationshipsIT extends RelationshipsITBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index d311a8d6c..1a4dbefbf 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.tuple; - import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; @@ -53,6 +49,10 @@ import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; import org.neo4j.driver.types.Relationship; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -120,9 +120,9 @@ import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.tuple; /** * @author Gerrit Meier @@ -133,32 +133,290 @@ import reactor.test.StepVerifier; @ExtendWith(Neo4jExtension.class) @SpringJUnitConfig @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) -@DirtiesContext // We need this here as the nested tests all inherit from the integration test base but the database selection here is different +@DirtiesContext // We need this here as the nested tests all inherit from the integration + // test base but the database selection here is different class ReactiveRepositoryIT { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - protected static final ThreadLocal databaseSelection = ThreadLocal.withInitial(DatabaseSelection::undecided); - protected static final ThreadLocal userSelection = ThreadLocal.withInitial(UserSelection::connectedUser); + protected static final ThreadLocal databaseSelection = ThreadLocal + .withInitial(DatabaseSelection::undecided); + + protected static final ThreadLocal userSelection = ThreadLocal + .withInitial(UserSelection::connectedUser); private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; + private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; + private static final String TEST_PERSON2_FIRST_NAME = "Bert"; + private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); + private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); + private static final String TEST_PERSON_SAMEVALUE = "SameValue"; + private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint(); + private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint(); + private static final long NOT_EXISTING_NODE_ID = 3123131231L; + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private long id1; + + private long id2; + + private PersonWithAllConstructor person1; + + private PersonWithAllConstructor person2; + static PersonWithAllConstructor personExample(String sameValue) { return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null, null); } - private long id1; - private long id2; - private PersonWithAllConstructor person1; - private PersonWithAllConstructor person2; + interface BidirectionalExternallyGeneratedIdRepository + extends ReactiveNeo4jRepository { + + } + + interface BidirectionalAssignedIdRepository extends ReactiveNeo4jRepository { + + } + + interface BidirectionalStartRepository extends ReactiveNeo4jRepository { + + } + + interface BidirectionalEndRepository extends ReactiveNeo4jRepository { + + } + + interface ImmutablePersonRepository extends ReactiveNeo4jRepository { + + } + + interface ReactiveLoopingRelationshipRepository + extends ReactiveNeo4jRepository { + + } + + interface ReactiveMultipleLabelRepository + extends ReactiveNeo4jRepository { + + } + + interface ReactiveMultipleLabelWithAssignedIdRepository + extends ReactiveNeo4jRepository { + + } + + interface ReactivePersonWithRelationshipWithPropertiesRepository + extends ReactiveNeo4jRepository { + + @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") + Mono loadFromCustomQuery(@Param("id") Long id); + + Mono findByHobbiesSince(int since); + + Mono findByHobbiesSinceOrHobbiesActive(int since1, boolean active); + + Mono findByHobbiesSinceAndHobbiesActive(int since1, boolean active); + + @Query("MATCH (p:PersonWithRelationshipWithProperties) return p {.name}") + Mono justTheNames(); + + } + + interface ReactiveHobbyWithRelationshipWithPropertiesRepository extends ReactiveNeo4jRepository { + + @Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)") + Flux loadFromCustomQuery(@Param("personId") Long personId); + + } + + interface ReactivePetRepository extends ReactiveNeo4jRepository { + + Mono countByName(String name); + + Mono existsByName(String name); + + Mono countByFriendsNameAndFriendsFriendsName(String friendName, String friendFriendName); + + } + + interface ReactiveRelationshipRepository extends ReactiveNeo4jRepository { + + @Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) " + + "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets " + + "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) " + + "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies") + Mono getPersonWithRelationshipsViaQuery(); + + Mono findByPetsName(String petName); + + Mono findByHobbiesNameOrPetsName(String hobbyName, String petName); + + Mono findByHobbiesNameAndPetsName(String hobbyName, String petName); + + Mono findByPetsHobbiesName(String hobbyName); + + Mono findByPetsFriendsName(String petName); + + Flux findByName(String name, Sort sort); + + Mono findDistinctByHobbiesName(String hobbyName); + + } + + interface ReactiveSimilarThingRepository extends ReactiveCrudRepository { + + } + + interface EntityWithConvertedIdRepository + extends ReactiveNeo4jRepository { + + } + + interface ThingWithFixedGeneratedIdRepository extends ReactiveNeo4jRepository { + + } + + interface EntityWithCustomIdAndDynamicLabelsRepository + extends ReactiveNeo4jRepository { + + } + + @SpringJUnitConfig(Config.class) + abstract static class ReactiveIntegrationTestBase { + + @Autowired + private Driver driver; + + @Autowired + private TransactionalOperator transactionalOperator; + + @Autowired + private BookmarkCapture bookmarkCapture; + + void setupData(TransactionContext transaction) { + } + + @BeforeEach + void before() { + doWithSession(session -> session.executeWrite(tx -> { + tx.run("MATCH (n) detach delete n").consume(); + setupData(tx); + return null; + })); + } + + T doWithSession(Function sessionConsumer) { + try (Session session = this.driver.session(this.bookmarkCapture + .createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { + T result = sessionConsumer.apply(session); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + return result; + } + } + + void assertInSession(Consumer consumer) { + + try (Session session = this.driver.session(this.bookmarkCapture + .createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { + consumer.accept(session); + } + } + + ReactiveSession createRxSession() { + + return this.driver.session(ReactiveSession.class, this.bookmarkCapture + .createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue())); + } + + TransactionalOperator getTransactionalOperator() { + return this.transactionalOperator; + } + + } + + @Configuration + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + @EnableTransactionManagement + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Override + public Collection getMappingBasePackages() { + return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + return ReactiveNeo4jTransactionManager.with(driver) + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .withBookmarkManager(Neo4jBookmarkManager.createReactive(bookmarkCapture())) + .build(); + } + + @Override + public ReactiveNeo4jClient neo4jClient(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + return ReactiveNeo4jClient.with(driver) + .withDatabaseSelectionProvider(databaseSelectionProvider) + .withUserSelectionProvider(getUserSelectionProvider()) + .build(); + } + + @Bean + TransactionalOperator transactionalOperator(ReactiveTransactionManager reactiveTransactionManager) { + return TransactionalOperator.create(reactiveTransactionManager); + } + + @Override + @Bean + public ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider() { + return Optional.ofNullable(databaseSelection.get().getValue()) // The thread + // local must + // be resolved + // early, + // before the + // mono + .map(ReactiveDatabaseSelectionProvider::createStaticDatabaseSelectionProvider) + .orElse(ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider()); + } + + @Bean + ReactiveUserSelectionProvider getUserSelectionProvider() { + return Optional.ofNullable(userSelection.get()) // The thread local must be + // resolved early, before the + // mono + .map(u -> (ReactiveUserSelectionProvider) () -> Mono.just(u)) + .orElse(ReactiveUserSelectionProvider.getDefaultSelectionProvider()); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } @Nested class Find extends ReactiveIntegrationTestBase { @@ -166,32 +424,41 @@ class ReactiveRepositoryIT { @Override void setupData(TransactionContext transaction) { - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ)) - .next().get(0).asLong(); + ReactiveRepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) + .next() + .get(0) + .asLong(); - id2 = transaction.run( + ReactiveRepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); - transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); - IntStream.rangeClosed(1, 20).forEach(i -> transaction - .run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values - .parameters("i", String.format("%02d", i)))); + transaction + .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); + IntStream.rangeClosed(1, 20) + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); + ReactiveRepositoryIT.this.person1 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id1, + TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, + "something", Arrays.asList("a", "b"), NEO4J_HQ, null); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + ReactiveRepositoryIT.this.person2 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id2, + TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, + null, Collections.emptyList(), SFO, null); transaction.run(""" CREATE (lhr:Airport {code: 'LHR', name: 'London Heathrow'}) @@ -212,15 +479,20 @@ class ReactiveRepositoryIT { @Test void findAll(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.findAll()).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier.create(repository.findAll()) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test void findById(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findById(id1)).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.findById(ReactiveRepositoryIT.this.id1)) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test @@ -231,23 +503,30 @@ class ReactiveRepositoryIT { int limit = 1; StepVerifier.create(repository.findByNameStartingWith("Test", PageRequest.of(page, limit, sort))) - .assertNext(person -> assertThat(person).isEqualTo(person1)).verifyComplete(); + .assertNext(person -> assertThat(person).isEqualTo(ReactiveRepositoryIT.this.person1)) + .verifyComplete(); sort = Sort.by("name"); page = 1; limit = 1; StepVerifier.create(repository.findByNameStartingWith("Test", PageRequest.of(page, limit, sort))) - .assertNext(person -> assertThat(person).isEqualTo(person2)).verifyComplete(); + .assertNext(person -> assertThat(person).isEqualTo(ReactiveRepositoryIT.this.person2)) + .verifyComplete(); } @Test void findAllByIds(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.findAllById(Arrays.asList(id1, id2))).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier + .create(repository + .findAllById(Arrays.asList(ReactiveRepositoryIT.this.id1, ReactiveRepositoryIT.this.id2))) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test @@ -265,10 +544,14 @@ class ReactiveRepositoryIT { @Test void findAllByIdsPublisher(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.findAllById(Flux.just(id1, id2))).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier + .create(repository.findAllById(Flux.just(ReactiveRepositoryIT.this.id1, ReactiveRepositoryIT.this.id2))) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test @@ -278,7 +561,9 @@ class ReactiveRepositoryIT { @Test void findByIdPublisher(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findById(Mono.just(id1))).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.findById(Mono.just(ReactiveRepositoryIT.this.id1))) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test @@ -288,82 +573,91 @@ class ReactiveRepositoryIT { @Test void findAllWithSortByOrderDefault(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findAll(Sort.by("name"))).expectNext(person1, person2).verifyComplete(); + StepVerifier.create(repository.findAll(Sort.by("name"))) + .expectNext(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2) + .verifyComplete(); } @Test void findAllWithSortByOrderAsc(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findAll(Sort.by(Sort.Order.asc("name")))).expectNext(person1, person2) - .verifyComplete(); + StepVerifier.create(repository.findAll(Sort.by(Sort.Order.asc("name")))) + .expectNext(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2) + .verifyComplete(); } @Test void findAllWithSortByOrderDesc(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findAll(Sort.by(Sort.Order.desc("name")))).expectNext(person2, person1) - .verifyComplete(); + StepVerifier.create(repository.findAll(Sort.by(Sort.Order.desc("name")))) + .expectNext(ReactiveRepositoryIT.this.person2, ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test void findOneByExample(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); - StepVerifier.create(repository.findOne(example)).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.findOne(example)) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test // GH-2343 void findOneByExampleFluent(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.one()) - .as(StepVerifier::create) - .expectNext(person1) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test void findAllByExample(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); - StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test // GH-2343 void findAllByExampleFluent(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, FluentQuery.ReactiveFluentQuery::all) - .as(StepVerifier::create) - .expectNext(person1) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test // GH-2343 void findAllByExampleFluentProjecting(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.project("name", "firstName").all()) - .as(StepVerifier::create) - .expectNextMatches(p -> { - assertThat(p.getName()).isEqualTo(person1.getName()); - assertThat(p.getFirstName()).isEqualTo(person1.getFirstName()); - assertThat(p.getId()).isNotNull(); + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getName()).isEqualTo(ReactiveRepositoryIT.this.person1.getName()); + assertThat(p.getFirstName()).isEqualTo(ReactiveRepositoryIT.this.person1.getFirstName()); + assertThat(p.getId()).isNotNull(); - assertThat(p.getBornOn()).isNull(); - assertThat(p.getCool()).isNull(); - assertThat(p.getCreatedAt()).isNull(); - assertThat(p.getNullable()).isNull(); - assertThat(p.getPersonNumber()).isNull(); - assertThat(p.getPlace()).isNull(); - assertThat(p.getSameValue()).isNull(); - assertThat(p.getThings()).isNull(); - return true; - }).verifyComplete(); + assertThat(p.getBornOn()).isNull(); + assertThat(p.getCool()).isNull(); + assertThat(p.getCreatedAt()).isNull(); + assertThat(p.getNullable()).isNull(); + assertThat(p.getPersonNumber()).isNull(); + assertThat(p.getPlace()).isNull(); + assertThat(p.getSameValue()).isNull(); + assertThat(p.getThings()).isNull(); + return true; + }) + .verifyComplete(); } @Test @@ -373,64 +667,66 @@ class ReactiveRepositoryIT { ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.project("name", "departure.name").all()) - .as(StepVerifier::create) - .expectNextMatches(p -> { - assertThat(p.getName()).isEqualTo("FL 001"); - assertThat(p.getArrival()).isNull(); - assertThat(p.getDeparture()).isNotNull(); - assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); - assertThat(p.getDeparture().getCode()).isNull(); + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getName()).isEqualTo("FL 001"); + assertThat(p.getArrival()).isNull(); + assertThat(p.getDeparture()).isNotNull(); + assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); + assertThat(p.getDeparture().getCode()).isNull(); - return true; - }).verifyComplete(); + return true; + }) + .verifyComplete(); } @Test // GH-2343 void findAllByExampleFluentAs(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.as(DtoPersonProjection.class).all()) - .map(DtoPersonProjection::getFirstName) - .as(StepVerifier::create) - .expectNext(TEST_PERSON1_FIRST_NAME) - .verifyComplete(); + .map(DtoPersonProjection::getFirstName) + .as(StepVerifier::create) + .expectNext(TEST_PERSON1_FIRST_NAME) + .verifyComplete(); } @Test // GH-2343 void findFirstByExample(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1, + Example example = Example.of(ReactiveRepositoryIT.this.person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).first()) - .as(StepVerifier::create) - .expectNext(person1) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test // GH-2726 void scrollByExample(@Autowired ReactivePersonRepository repository) { - PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); + PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, + TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); Example example = Example.of(sameValuePerson, ExampleMatcher.matchingAll().withIgnoreNullValues()); repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset(0))) - .as(StepVerifier::create) - .expectNextMatches(person -> { - assertThat(person).isNotNull(); - assertThat(person.getContent().get(0)).isEqualTo(person1); + .as(StepVerifier::create) + .expectNextMatches(person -> { + assertThat(person).isNotNull(); + assertThat(person.getContent().get(0)).isEqualTo(ReactiveRepositoryIT.this.person1); - ScrollPosition currentPosition = person.positionAt(person1); - repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition)) - .as(StepVerifier::create) - .expectNextMatches(nextPerson -> { - assertThat(nextPerson.getContent().get(0)).isEqualTo(person2); - return true; - }); - return true; - }); + ScrollPosition currentPosition = person.positionAt(ReactiveRepositoryIT.this.person1); + repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition)) + .as(StepVerifier::create) + .expectNextMatches(nextPerson -> { + assertThat(nextPerson.getContent().get(0)).isEqualTo(ReactiveRepositoryIT.this.person2); + return true; + }); + return true; + }); } @Test @@ -438,40 +734,58 @@ class ReactiveRepositoryIT { PersonWithAllConstructor person; Example example; - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, - null, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, + null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); - StepVerifier.create(repository.findAll(example)).recordWith(ArrayList::new).expectNextCount(2) - .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) - .verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons + .containsAll(Arrays.asList(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2))) + .verifyComplete(); - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, null, - null, null, null, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, + null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); - StepVerifier.create(repository.findAll(example)).recordWith(ArrayList::new).expectNextCount(2) - .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) - .verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons + .containsAll(Arrays.asList(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2))) + .verifyComplete(); person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), - TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null, null, - null, null); - example = Example.of(person, ExampleMatcher.matchingAll() - .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING, true)) - .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)) - .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))); + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, + null, null, null, null); + example = Example.of(person, + ExampleMatcher.matchingAll() + .withMatcher("name", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING, true)) + .withMatcher("firstName", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)) + .withMatcher("sameValue", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))); - StepVerifier.create(repository.findAll(example)).expectNext(person2).verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .expectNext(ReactiveRepositoryIT.this.person2) + .verifyComplete(); - person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null, null); - example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX)); + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null, + null); + example = Example.of(person, + ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX)); - StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); example = Example.of(person, - ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX).withIncludeNullValues()); + ExampleMatcher.matchingAll() + .withStringMatcher(ExampleMatcher.StringMatcher.REGEX) + .withIncludeNullValues()); StepVerifier.create(repository.findAll(example)).verifyComplete(); } @@ -481,31 +795,30 @@ class ReactiveRepositoryIT { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); StepVerifier.create(repository.findAll(example, Sort.by(Sort.Direction.DESC, "name"))) - .expectNext(person2, person1).verifyComplete(); + .expectNext(ReactiveRepositoryIT.this.person2, ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test // GH-2343 void findAllByExampleWithSortFluent(@Autowired ReactivePersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); - repository - .findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()) - .as(StepVerifier::create) - .expectNext(person2, person1) - .verifyComplete(); + repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()) + .as(StepVerifier::create) + .expectNext(ReactiveRepositoryIT.this.person2, ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test void findEntityWithRelationshipByFindOneByExample(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -519,34 +832,31 @@ class ReactiveRepositoryIT { PersonWithRelationship probe = new PersonWithRelationship(); probe.setName("Freddie"); - StepVerifier.create(repository.findOne(Example.of(probe))) - .assertNext(loadedPerson -> { - assertThat(loadedPerson.getName()).isEqualTo("Freddie"); - assertThat(loadedPerson.getId()).isEqualTo(personId); - Hobby hobby = loadedPerson.getHobbies(); - assertThat(hobby).isNotNull(); - assertThat(hobby.getId()).isEqualTo(hobbyNodeId); - assertThat(hobby.getName()).isEqualTo("Music"); + StepVerifier.create(repository.findOne(Example.of(probe))).assertNext(loadedPerson -> { + assertThat(loadedPerson.getName()).isEqualTo("Freddie"); + assertThat(loadedPerson.getId()).isEqualTo(personId); + Hobby hobby = loadedPerson.getHobbies(); + assertThat(hobby).isNotNull(); + assertThat(hobby.getId()).isEqualTo(hobbyNodeId); + assertThat(hobby.getName()).isEqualTo("Music"); - List pets = loadedPerson.getPets(); - Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); - Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); - assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); - }) - .verifyComplete(); + List pets = loadedPerson.getPets(); + Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); + Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); + assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); + }).verifyComplete(); } @Test void findEntityWithRelationshipByFindAllByExample(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -560,67 +870,63 @@ class ReactiveRepositoryIT { PersonWithRelationship probe = new PersonWithRelationship(); probe.setName("Freddie"); - StepVerifier.create(repository.findAll(Example.of(probe))) - .assertNext(loadedPerson -> { - assertThat(loadedPerson.getName()).isEqualTo("Freddie"); - assertThat(loadedPerson.getId()).isEqualTo(personId); - Hobby hobby = loadedPerson.getHobbies(); - assertThat(hobby).isNotNull(); - assertThat(hobby.getId()).isEqualTo(hobbyNodeId); - assertThat(hobby.getName()).isEqualTo("Music"); + StepVerifier.create(repository.findAll(Example.of(probe))).assertNext(loadedPerson -> { + assertThat(loadedPerson.getName()).isEqualTo("Freddie"); + assertThat(loadedPerson.getId()).isEqualTo(personId); + Hobby hobby = loadedPerson.getHobbies(); + assertThat(hobby).isNotNull(); + assertThat(hobby.getId()).isEqualTo(hobbyNodeId); + assertThat(hobby.getName()).isEqualTo("Music"); - List pets = loadedPerson.getPets(); - Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); - Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); - assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); - }) - .verifyComplete(); + List pets = loadedPerson.getPets(); + Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); + Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); + assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); + }).verifyComplete(); } @Test - void findEntityWithRelationshipByFindAllByExampleWithSort(@Autowired ReactiveRelationshipRepository repository) { + void findEntityWithRelationshipByFindAllByExampleWithSort( + @Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node petNode1 = record.get("p1").asNode(); Node petNode2 = record.get("p2").asNode(); - long personId = TestIdentitySupport.getInternalId(personNode); + long personId = TestIdentitySupport.getInternalId(personNode); long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1); - long petNode1Id = TestIdentitySupport.getInternalId(petNode1); - long petNode2Id = TestIdentitySupport.getInternalId(petNode2); + long petNode1Id = TestIdentitySupport.getInternalId(petNode1); + long petNode2Id = TestIdentitySupport.getInternalId(petNode2); PersonWithRelationship probe = new PersonWithRelationship(); probe.setName("Freddie"); - StepVerifier.create(repository.findAll(Example.of(probe), Sort.by("name"))) - .assertNext(loadedPerson -> { - assertThat(loadedPerson.getName()).isEqualTo("Freddie"); - assertThat(loadedPerson.getId()).isEqualTo(personId); - Hobby hobby = loadedPerson.getHobbies(); - assertThat(hobby).isNotNull(); - assertThat(hobby.getId()).isEqualTo(hobbyNodeId); - assertThat(hobby.getName()).isEqualTo("Music"); + StepVerifier.create(repository.findAll(Example.of(probe), Sort.by("name"))).assertNext(loadedPerson -> { + assertThat(loadedPerson.getName()).isEqualTo("Freddie"); + assertThat(loadedPerson.getId()).isEqualTo(personId); + Hobby hobby = loadedPerson.getHobbies(); + assertThat(hobby).isNotNull(); + assertThat(hobby.getId()).isEqualTo(hobbyNodeId); + assertThat(hobby.getName()).isEqualTo("Music"); - List pets = loadedPerson.getPets(); - Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); - Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); - assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); - }) - .verifyComplete(); + List pets = loadedPerson.getPets(); + Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); + Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); + assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); + }).verifyComplete(); } @Test void existsById(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); + StepVerifier.create(repository.existsById(ReactiveRepositoryIT.this.id1)).expectNext(true).verifyComplete(); } @Test @@ -630,7 +936,7 @@ class ReactiveRepositoryIT { @Test void existsByIdPublisher(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); + StepVerifier.create(repository.existsById(ReactiveRepositoryIT.this.id1)).expectNext(true).verifyComplete(); } @Test @@ -643,16 +949,16 @@ class ReactiveRepositoryIT { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); repository.findBy(example, q -> q.page(PageRequest.of(1, 1, Sort.by("name")))) - .as(StepVerifier::create) - .expectNextMatches(page -> { - assertThat(page).containsExactly(person2); - assertThat(page.getTotalPages()).isEqualTo(2L); - assertThat(page.getTotalElements()).isEqualTo(2L); - assertThat(page.hasPrevious()).isTrue(); - assertThat(page.hasNext()).isFalse(); - return true; - }) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(page -> { + assertThat(page).containsExactly(ReactiveRepositoryIT.this.person2); + assertThat(page.getTotalPages()).isEqualTo(2L); + assertThat(page.getTotalElements()).isEqualTo(2L); + assertThat(page.hasPrevious()).isTrue(); + assertThat(page.hasNext()).isFalse(); + return true; + }) + .verifyComplete(); } @Test @@ -665,10 +971,7 @@ class ReactiveRepositoryIT { void existsByExampleFluent(@Autowired ReactivePersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); - repository.findBy(example, q -> q.exists()) - .as(StepVerifier::create) - .expectNext(true) - .verifyComplete(); + repository.findBy(example, q -> q.exists()).as(StepVerifier::create).expectNext(true).verifyComplete(); } @Test @@ -678,18 +981,15 @@ class ReactiveRepositoryIT { @Test void countByExample(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1); + Example example = Example.of(ReactiveRepositoryIT.this.person1); StepVerifier.create(repository.count(example)).expectNext(1L).verifyComplete(); } @Test // GH-2343 void countByExampleFluent(@Autowired ReactivePersonRepository repository) { - Example example = Example.of(person1); - repository.findBy(example, q -> q.count()) - .as(StepVerifier::create) - .expectNext(1L) - .verifyComplete(); + Example example = Example.of(ReactiveRepositoryIT.this.person1); + repository.findBy(example, q -> q.count()).as(StepVerifier::create).expectNext(1L).verifyComplete(); } @Test @@ -699,83 +999,107 @@ class ReactiveRepositoryIT { @Test void loadAllPersonsWithAllConstructor(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.getAllPersonsViaQuery()).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier.create(repository.getAllPersonsViaQuery()) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test // DATAGRAPH-1429 void aggregateThroughQueryIntoListShouldWork(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.aggregateAllPeople()).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier.create(repository.aggregateAllPeople()) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test // DATAGRAPH-1429 - void queryAggregatesShouldWorkWithTheTemplate(@Autowired ReactiveNeo4jTemplate template, @Autowired ReactiveTransactionManager reactiveTransactionManager) { + void queryAggregatesShouldWorkWithTheTemplate(@Autowired ReactiveNeo4jTemplate template, + @Autowired ReactiveTransactionManager reactiveTransactionManager) { - Flux people = TransactionalOperator.create(reactiveTransactionManager).transactional(template.findAll("unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", Person.class)); + Flux people = TransactionalOperator.create(reactiveTransactionManager) + .transactional(template.findAll( + "unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", + Person.class)); - StepVerifier.create(people.map(Person::getFirstName)) - .expectNext("1", "2", "3", "4", "5") - .verifyComplete(); + StepVerifier.create(people.map(Person::getFirstName)).expectNext("1", "2", "3", "4", "5").verifyComplete(); } @Test void loadOnePersonWithAllConstructor(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.getOnePersonViaQuery()).expectNext(person1).verifyComplete(); + StepVerifier.create(repository.getOnePersonViaQuery()) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test void findBySimplePropertiesAnded(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)) - .expectNext(person1).verifyComplete(); + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); - StepVerifier.create(repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), - TEST_PERSON1_FIRST_NAME.toUpperCase())).expectNext(person1).verifyComplete(); + StepVerifier + .create(repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), + TEST_PERSON1_FIRST_NAME.toUpperCase())) + .expectNext(ReactiveRepositoryIT.this.person1) + .verifyComplete(); } @Test void findBySimplePropertiesOred(@Autowired ReactivePersonRepository repository) { - repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME).as(StepVerifier::create) - .recordWith(ArrayList::new).expectNextCount(2) - .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) - .verifyComplete(); + repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) + .as(StepVerifier::create) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons + .containsAll(Arrays.asList(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2))) + .verifyComplete(); } @Test // GH-112 void countBySimplePropertiesOred(@Autowired ReactivePersonRepository repository) { - repository.countAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME).as(StepVerifier::create).expectNext(2L) - .verifyComplete(); + repository.countAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); } @Test void findBySimpleProperty(@Autowired ReactivePersonRepository repository) { - List personList = Arrays.asList(person1, person2); + List personList = Arrays.asList(ReactiveRepositoryIT.this.person1, + ReactiveRepositoryIT.this.person2); - StepVerifier.create(repository.findAllBySameValue(TEST_PERSON_SAMEVALUE)).expectNextMatches(personList::contains) - .expectNextMatches(personList::contains).verifyComplete(); + StepVerifier.create(repository.findAllBySameValue(TEST_PERSON_SAMEVALUE)) + .expectNextMatches(personList::contains) + .expectNextMatches(personList::contains) + .verifyComplete(); } @Test void findByPropertyThatNeedsConversion(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAllByPlace(new GeographicPoint2d(NEO4J_HQ.y(), NEO4J_HQ.x()))) - .expectNextCount(1).verifyComplete(); + .expectNextCount(1) + .verifyComplete(); } @Test void findByPropertyFailsIfNoConverterIsAvailable(@Autowired ReactivePersonRepository repository) { assertThatExceptionOfType(ConverterNotFoundException.class) - .isThrownBy(() -> repository.findAllByPlace(new ReactivePersonRepository.SomethingThatIsNotKnownAsEntity())) - .withMessageStartingWith("No converter found capable of converting from type"); + .isThrownBy( + () -> repository.findAllByPlace(new ReactivePersonRepository.SomethingThatIsNotKnownAsEntity())) + .withMessageStartingWith("No converter found capable of converting from type"); } @Test @@ -820,43 +1144,46 @@ class ReactiveRepositoryIT { doWithSession(session -> session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})").consume()); StepVerifier.create(repository.findAllById(Collections.singleton(EntityWithConvertedId.IdentifyingEnum.A))) - .assertNext( - entity -> assertThat(entity.getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A)) - .verifyComplete(); + .assertNext(entity -> assertThat(entity.getIdentifyingEnum()) + .isEqualTo(EntityWithConvertedId.IdentifyingEnum.A)) + .verifyComplete(); } @Test void loadOptionalPersonWithAllConstructorWithSpelParameters(@Autowired ReactivePersonRepository repository) { Flux person = repository - .getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + .getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); person.map(PersonWithAllConstructor::getName) - .as(StepVerifier::create) - .expectNext(TEST_PERSON1_NAME) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(TEST_PERSON1_NAME) + .verifyComplete(); } @Test - void loadOptionalPersonWithAllConstructorWithSpelParametersAndDynamicSort(@Autowired ReactivePersonRepository repository) { + void loadOptionalPersonWithAllConstructorWithSpelParametersAndDynamicSort( + @Autowired ReactivePersonRepository repository) { - Flux person = repository - .getOptionalPersonViaQueryWithSort(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2), Sort.by("n.name").ascending()); + Flux person = repository.getOptionalPersonViaQueryWithSort( + TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2), Sort.by("n.name").ascending()); person.map(PersonWithAllConstructor::getName) - .as(StepVerifier::create) - .expectNext(TEST_PERSON1_NAME) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(TEST_PERSON1_NAME) + .verifyComplete(); } @Test - void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery(@Autowired ReactivePersonRepository repository) { + void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery( + @Autowired ReactivePersonRepository repository) { Flux person = repository - .getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + .getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); person.map(PersonWithAllConstructor::getName) - .as(StepVerifier::create) - .expectNext(TEST_PERSON1_NAME) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(TEST_PERSON1_NAME) + .verifyComplete(); } + } @Nested @@ -867,47 +1194,54 @@ class ReactiveRepositoryIT { transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ)) - .next().get(0).asLong(); + ReactiveRepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) + .next() + .get(0) + .asLong(); - id2 = transaction.run( + ReactiveRepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); - transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); - IntStream.rangeClosed(1, 20).forEach(i -> transaction - .run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values - .parameters("i", String.format("%02d", i)))); + transaction + .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); + IntStream.rangeClosed(1, 20) + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); + ReactiveRepositoryIT.this.person1 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id1, + TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, + "something", Arrays.asList("a", "b"), NEO4J_HQ, null); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + ReactiveRepositoryIT.this.person2 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id2, + TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, + null, Collections.emptyList(), SFO, null); } @Test void loadEntityWithRelationship(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), - (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), - (p1)-[:Has]->(p2) - RETURN n, h1, h2, p1, p2, c - """) - .single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), + (n)<-[:Has]-(c:Club{name:'ClownsClub'}), (p1)-[:Has]->(h2:Hobby{name:'sleeping'}), + (p1)-[:Has]->(p2) + RETURN n, h1, h2, p1, p2, c + """).single()); Node personNode = record.get("n").asNode(); Node clubNode = record.get("c").asNode(); @@ -954,22 +1288,21 @@ class ReactiveRepositoryIT { @Test void findEntityWithRelationshipToTheSameNode(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (p1)-[:Has]->(h1) - RETURN n, h1, p1 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (p1)-[:Has]->(h1) + RETURN n, h1, p1 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node petNode1 = record.get("p1").asNode(); - long personId = TestIdentitySupport.getInternalId(personNode); + long personId = TestIdentitySupport.getInternalId(personNode); long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1); - long petNode1Id = TestIdentitySupport.getInternalId(petNode1); + long petNode1Id = TestIdentitySupport.getInternalId(petNode1); StepVerifier.create(repository.findById(personId)).assertNext(loadedPerson -> { @@ -992,23 +1325,26 @@ class ReactiveRepositoryIT { } @Test - void loadLoopingDeepRelationships(@Autowired ReactiveLoopingRelationshipRepository loopingRelationshipRepository) { + void loadLoopingDeepRelationships( + @Autowired ReactiveLoopingRelationshipRepository loopingRelationshipRepository) { long type1Id = doWithSession(session -> { - Record record = session.run(""" - CREATE - (t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> - (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->(:LoopingType1) - RETURN t1 - """).single(); + Record record = session + .run(""" + CREATE + (t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]-> + (:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->(:LoopingType1) + RETURN t1 + """) + .single(); return TestIdentitySupport.getInternalId(record.get("t1").asNode()); }); @@ -1042,18 +1378,21 @@ class ReactiveRepositoryIT { void loadEntityWithBidirectionalRelationship(@Autowired BidirectionalStartRepository repository) { Node startNode = doWithSession(session -> { - Record record = session.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN n").single(); + Record record = session.run( + "CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN n") + .single(); return record.get("n").asNode(); }); StepVerifier.create(repository.findById(TestIdentitySupport.getInternalId(startNode))) - .verifyErrorMatches(error -> { - Throwable cause = error.getCause(); - return cause instanceof MappingException && cause.getMessage().equals( - "The node with id " + startNode.elementId() + " has a logical cyclic mapping dependency; " + - "its creation caused the creation of another node that has a reference to this"); - }); + .verifyErrorMatches(error -> { + Throwable cause = error.getCause(); + return cause instanceof MappingException && cause.getMessage() + .equals("The node with id " + startNode.elementId() + + " has a logical cyclic mapping dependency; " + + "its creation caused the creation of another node that has a reference to this"); + }); } @@ -1061,7 +1400,9 @@ class ReactiveRepositoryIT { void loadEntityWithBidirectionalRelationshipFromIncomingSide(@Autowired BidirectionalEndRepository repository) { long endId = doWithSession(session -> { - Record record = session.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e").single(); + Record record = session.run( + "CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e") + .single(); Node endNode = record.get("e").asNode(); return TestIdentitySupport.getInternalId(endNode); @@ -1075,54 +1416,56 @@ class ReactiveRepositoryIT { @Test void loadMultipleEntitiesWithRelationship(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p") - .single()); + Record record = doWithSession(session -> session.run( + "CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p") + .single()); long hobbyNode1Id = TestIdentitySupport.getInternalId(record.get("h").asNode()); long petNode1Id = TestIdentitySupport.getInternalId(record.get("p").asNode()); record = doWithSession(session -> session.run(""" - CREATE - (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), - (n)-[:Has]->(p:Pet{name: 'Jerry2'}) - RETURN n, h, p - """).single()); + CREATE + (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), + (n)-[:Has]->(p:Pet{name: 'Jerry2'}) + RETURN n, h, p + """).single()); long hobbyNode2Id = TestIdentitySupport.getInternalId(record.get("h").asNode()); long petNode2Id = TestIdentitySupport.getInternalId(record.get("p").asNode()); - StepVerifier.create(repository.findAll()).recordWith(ArrayList::new).expectNextCount(2) - .consumeRecordedWith(loadedPersons -> { + StepVerifier.create(repository.findAll()) + .recordWith(ArrayList::new) + .expectNextCount(2) + .consumeRecordedWith(loadedPersons -> { - Hobby hobby1 = new Hobby(); - hobby1.setId(hobbyNode1Id); - hobby1.setName("Music"); + Hobby hobby1 = new Hobby(); + hobby1.setId(hobbyNode1Id); + hobby1.setName("Music"); - Hobby hobby2 = new Hobby(); - hobby2.setId(hobbyNode2Id); - hobby2.setName("Music2"); + Hobby hobby2 = new Hobby(); + hobby2.setId(hobbyNode2Id); + hobby2.setName("Music2"); - Pet pet1 = new Pet(petNode1Id, "Jerry"); - Pet pet2 = new Pet(petNode2Id, "Jerry2"); + Pet pet1 = new Pet(petNode1Id, "Jerry"); + Pet pet2 = new Pet(petNode2Id, "Jerry2"); - assertThat(loadedPersons).extracting("name").containsExactlyInAnyOrder("Freddie", "SomeoneElse"); - assertThat(loadedPersons).extracting("hobbies").containsExactlyInAnyOrder(hobby1, hobby2); - assertThat(loadedPersons).flatExtracting("pets").containsExactlyInAnyOrder(pet1, pet2); - }).verifyComplete(); + assertThat(loadedPersons).extracting("name").containsExactlyInAnyOrder("Freddie", "SomeoneElse"); + assertThat(loadedPersons).extracting("hobbies").containsExactlyInAnyOrder(hobby1, hobby2); + assertThat(loadedPersons).flatExtracting("pets").containsExactlyInAnyOrder(pet1, pet2); + }) + .verifyComplete(); } @Test void loadEntityWithRelationshipViaQuery(@Autowired ReactiveRelationshipRepository repository) { - Record record = doWithSession(session -> session - .run(""" - CREATE - (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), - (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) - RETURN n, h1, p1, p2 - """).single()); + Record record = doWithSession(session -> session.run(""" + CREATE + (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), + (n)-[:Has]->(p1:Pet{name: 'Jerry'}), + (n)-[:Has]->(p2:Pet{name: 'Tom'}) + RETURN n, h1, p1, p2 + """).single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1153,7 +1496,9 @@ class ReactiveRepositoryIT { void loadEntityWithRelationshipWithAssignedId(@Autowired ReactivePetRepository repository) { long petNodeId = doWithSession(session -> { - Record record = session.run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t").single(); + Record record = session + .run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t") + .single(); Node petNode = record.get("p").asNode(); return TestIdentitySupport.getInternalId(petNode); @@ -1187,8 +1532,8 @@ class ReactiveRepositoryIT { void countByPatternPathProperties(@Autowired ReactivePetRepository repository) { createFriendlyPets(); StepVerifier.create(repository.countByFriendsNameAndFriendsFriendsName("Daphne", "Tom")) - .expectNextCount(1L) - .verifyComplete(); + .expectNextCount(1L) + .verifyComplete(); } @Test // GH-2157 @@ -1198,10 +1543,14 @@ class ReactiveRepositoryIT { } private long createFriendlyPets() { - return doWithSession(session -> session.run(""" - CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(:Pet{name:'Tom'}) - RETURN id(luna) as id - """).single().get("id").asLong()); + return doWithSession(session -> session + .run(""" + CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(:Pet{name:'Tom'}) + RETURN id(luna) as id + """) + .single() + .get("id") + .asLong()); } @Test // GH-2175 @@ -1214,9 +1563,7 @@ class ReactiveRepositoryIT { (p1)-[:Has]->(p2) """).consume()); - StepVerifier.create(repository.findAll(Sort.by("name"))) - .expectNextCount(1) - .verifyComplete(); + StepVerifier.create(repository.findAll(Sort.by("name"))).expectNextCount(1).verifyComplete(); } @Test // GH-2175 @@ -1229,25 +1576,27 @@ class ReactiveRepositoryIT { (p1)-[:Has]->(p2) """).consume()); - StepVerifier.create(repository.findByName("Freddie", Sort.by("name"))) - .expectNextCount(1) - .verifyComplete(); + StepVerifier.create(repository.findByName("Freddie", Sort.by("name"))).expectNextCount(1).verifyComplete(); } + } @Nested class RelationshipProperties extends ReactiveIntegrationTestBase { @Test - void loadEntityWithRelationshipWithProperties(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { + void loadEntityWithRelationshipWithProperties( + @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - Record record = doWithSession(session -> session.run(""" - CREATE - (n:PersonWithRelationshipWithProperties{name:'Freddie'}), - (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), - (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) - RETURN n, h1, h2 - """).single()); + Record record = doWithSession(session -> session + .run(""" + CREATE + (n:PersonWithRelationshipWithProperties{name:'Freddie'}), + (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), + (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) + RETURN n, h1, h2 + """) + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1289,7 +1638,8 @@ class ReactiveRepositoryIT { } @Test - void saveEntityWithRelationshipWithProperties(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { + void saveEntityWithRelationshipWithProperties( + @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { // given Hobby h1 = new Hobby(); h1.setName("Music"); @@ -1330,8 +1680,8 @@ class ReactiveRepositoryIT { club.setName("BlubbClub"); WorksInClubRelationship worksInClub = new WorksInClubRelationship(2002, club); - PersonWithRelationshipWithProperties person = - new PersonWithRelationshipWithProperties("Freddie clone", hobbies, worksInClub); + PersonWithRelationshipWithProperties person = new PersonWithRelationshipWithProperties("Freddie clone", + hobbies, worksInClub); // when Mono operationUnderTest = repository.save(person); @@ -1339,57 +1689,66 @@ class ReactiveRepositoryIT { // then List shouldBeDifferentPersons = new ArrayList<>(); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create) - .recordWith(() -> shouldBeDifferentPersons).expectNextCount(1L).verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> shouldBeDifferentPersons) + .expectNextCount(1L) + .verifyComplete(); assertThat(shouldBeDifferentPersons).size().isEqualTo(1); PersonWithRelationshipWithProperties shouldBeDifferentPerson = shouldBeDifferentPersons.get(0); assertThat(shouldBeDifferentPerson).isNotNull() - .usingRecursiveComparison() - .ignoringFieldsMatchingRegexes("^(?:(?!hobbies).)*$") - .isEqualTo(person); + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("^(?:(?!hobbies).)*$") + .isEqualTo(person); assertThat(shouldBeDifferentPerson.getName()).isEqualToIgnoringCase("Freddie clone"); // check content of db String matchQuery = """ - MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) - RETURN n, [(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, [(n) -[r:LIKES]->(:Hobby) |r] as rels - """; - Flux.usingWhen(Mono.fromSupplier(this::createRxSession), s -> Flux.from(s.run(matchQuery)).flatMap(rs -> Flux.from(rs.records())), s -> Mono.fromDirect(s.close())) - .as(StepVerifier::create).assertNext(record -> { + MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) + RETURN n, [(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, [(n) -[r:LIKES]->(:Hobby) |r] as rels + """; + Flux.usingWhen(Mono.fromSupplier(this::createRxSession), + s -> Flux.from(s.run(matchQuery)).flatMap(rs -> Flux.from(rs.records())), + s -> Mono.fromDirect(s.close())) + .as(StepVerifier::create) + .assertNext(record -> { - assertThat(record.containsKey("n")).isTrue(); - assertThat(record.containsKey("Hobbies")).isTrue(); - assertThat(record.containsKey("rels")).isTrue(); - assertThat(record.values()).hasSize(3); - assertThat(record.get("Hobbies").values()).hasSize(2); - assertThat(record.get("rels").values()).hasSize(2); + assertThat(record.containsKey("n")).isTrue(); + assertThat(record.containsKey("Hobbies")).isTrue(); + assertThat(record.containsKey("rels")).isTrue(); + assertThat(record.values()).hasSize(3); + assertThat(record.get("Hobbies").values()).hasSize(2); + assertThat(record.get("rels").values()).hasSize(2); - assertThat(record.get("rels").values(Value::asRelationship)) - .extracting(Relationship::type, rel -> rel.get("active"), rel -> rel.get("localDate"), - rel -> rel.get("point"), rel -> rel.get("myEnum"), rel -> rel.get("since")) - .containsExactlyInAnyOrder( - tuple("LIKES", Values.value(rel1Active), Values.value(rel1LocalDate), - Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()), - Values.value(rel1MyEnum.name()), Values.value(rel1Since)), - tuple("LIKES", Values.value(rel2Active), Values.value(rel2LocalDate), - Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()), - Values.value(rel2MyEnum.name()), Values.value(rel2Since))); - }).verifyComplete(); + assertThat(record.get("rels").values(Value::asRelationship)) + .extracting(Relationship::type, rel -> rel.get("active"), rel -> rel.get("localDate"), + rel -> rel.get("point"), rel -> rel.get("myEnum"), rel -> rel.get("since")) + .containsExactlyInAnyOrder( + tuple("LIKES", Values.value(rel1Active), Values.value(rel1LocalDate), + Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()), + Values.value(rel1MyEnum.name()), Values.value(rel1Since)), + tuple("LIKES", Values.value(rel2Active), Values.value(rel2LocalDate), + Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()), + Values.value(rel2MyEnum.name()), Values.value(rel2Since))); + }) + .verifyComplete(); } @Test void loadEntityWithRelationshipWithPropertiesFromCustomQuery( @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - Record record = doWithSession(session -> session.run(""" - CREATE - (n:PersonWithRelationshipWithProperties{name:'Freddie'}), - (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), - (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) - RETURN n, h1, h2 - """).single()); + Record record = doWithSession(session -> session + .run(""" + CREATE + (n:PersonWithRelationshipWithProperties{name:'Freddie'}), + (n)-[l1:LIKES {since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}]->(h1:Hobby{name:'Music'}), + (n)-[l2:LIKES {since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}]->(h2:Hobby{name:'Something else'}) + RETURN n, h1, h2 + """) + .single()); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); @@ -1435,7 +1794,9 @@ class ReactiveRepositoryIT { @Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) { long personId = doWithSession(session -> { - Record record = session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single(); + Record record = session.run( + "CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1") + .single(); return TestIdentitySupport.getInternalId(record.get("n").asNode()); }); @@ -1450,13 +1811,15 @@ class ReactiveRepositoryIT { } @Test - void loadSameNodeWithDoubleRelationship(@Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) { + void loadSameNodeWithDoubleRelationship( + @Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) { long personId = doWithSession(session -> { - Record record = session.run("CREATE (n:AltPerson{name:'Freddie'})," + - " (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," + - " (n)-[l2:LIKES {rating: 1}]->(h1)" + - " RETURN n, h1").single(); + Record record = session + .run("CREATE (n:AltPerson{name:'Freddie'})," + + " (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," + + " (n)-[l2:LIKES {rating: 1}]->(h1)" + " RETURN n, h1") + .single(); return TestIdentitySupport.getInternalId(record.get("n").asNode()); }); @@ -1478,6 +1841,7 @@ class ReactiveRepositoryIT { assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2); }); } + } @Nested @@ -1485,101 +1849,112 @@ class ReactiveRepositoryIT { @Test void findByPropertyOnRelatedEntity(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})").consume()); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})") + .consume()); StepVerifier.create(repository.findByPetsName("Jerry")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); } @Test void findByPropertyOnRelatedEntitiesOr(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," - + "(n)-[:Has]->(:Hobby{name: 'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + + "(n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); StepVerifier.create(repository.findByHobbiesNameOrPetsName("Music", "Jerry")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameOrPetsName("Sports", "Tom")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameOrPetsName("Sports", "Jerry")).verifyComplete(); } @Test void findByPropertyOnRelatedEntitiesAnd(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," - + "(n)-[:Has]->(:Hobby{name: 'Music'})").consume() - ); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + + "(n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); StepVerifier.create(repository.findByHobbiesNameAndPetsName("Music", "Tom")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameAndPetsName("Sports", "Jerry")).verifyComplete(); } @Test void findByPropertyOnRelatedEntityOfRelatedEntity(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" - + "-[:Has]->(:Hobby{name: 'Sleeping'})").consume() - ); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + + "-[:Has]->(:Hobby{name: 'Sleeping'})") + .consume()); StepVerifier.create(repository.findByPetsHobbiesName("Sleeping")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByPetsHobbiesName("Sports")).verifyComplete(); } @Test void findByPropertyOnRelatedEntityOfRelatedSameEntity(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" - + "-[:Has]->(:Pet{name: 'Tom'})").consume() - ); + doWithSession(session -> session + .run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + + "-[:Has]->(:Pet{name: 'Tom'})") + .consume()); StepVerifier.create(repository.findByPetsFriendsName("Tom")) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByPetsFriendsName("Jerry")).verifyComplete(); } @Test // GH-2243 void findDistinctByRelatedEntity(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Hobby{name: 'Music'})" - + "CREATE (n)-[:Has]->(:Hobby{name: 'Music'})").consume()); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Hobby{name: 'Music'})" + + "CREATE (n)-[:Has]->(:Hobby{name: 'Music'})") + .consume()); StepVerifier.create(repository.findDistinctByHobbiesName("Music")) - .assertNext(person -> assertThat(person).isNotNull()) - .verifyComplete(); + .assertNext(person -> assertThat(person).isNotNull()) + .verifyComplete(); } @Test void findByPropertyOnRelationshipWithProperties( @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})").consume() - ); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})") + .consume()); StepVerifier.create(repository.findByHobbiesSince(2020)) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); } @Test void findByPropertyOnRelationshipWithPropertiesOr( @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume() - ); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2020, false)) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2019, true)) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2019, false)).verifyComplete(); } @@ -1587,30 +1962,31 @@ class ReactiveRepositoryIT { @Test void findByCustomQueryOnlyWithPropertyReturn( @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume() - ); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); StepVerifier.create(repository.justTheNames()) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); } @Test void findByPropertyOnRelationshipWithPropertiesAnd( @Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { - doWithSession(session -> - session.run( - "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})").consume() - ); + doWithSession(session -> session.run( + "CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})") + .consume()); StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2020, true)) - .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")).verifyComplete(); + .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) + .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2019, true)).verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2020, false)).verifyComplete(); } + } @Nested @@ -1621,80 +1997,105 @@ class ReactiveRepositoryIT { transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ)) - .next().get(0).asLong(); + ReactiveRepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) + .next() + .get(0) + .asLong(); - id2 = transaction.run( + ReactiveRepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); - transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); - IntStream.rangeClosed(1, 20).forEach(i -> transaction - .run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values - .parameters("i", String.format("%02d", i)))); + transaction + .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); + IntStream.rangeClosed(1, 20) + .forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + Values.parameters("i", String.format("%02d", i)))); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); + ReactiveRepositoryIT.this.person1 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id1, + TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, + "something", Arrays.asList("a", "b"), NEO4J_HQ, null); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + ReactiveRepositoryIT.this.person2 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id2, + TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, + null, Collections.emptyList(), SFO, null); } @Test void saveSingleEntity(@Autowired ReactivePersonRepository repository) { - PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, - LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); + PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, + 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); Mono operationUnderTest = repository.save(person).map(PersonWithAllConstructor::getId); List ids = new ArrayList<>(); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> ids) - .expectNextCount(1L).verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), - s -> Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n", Values - .parameters("ids", ids))).flatMap(ReactiveResult::records), + s -> Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n", + Values.parameters("ids", ids))) + .flatMap(ReactiveResult::records), s -> Mono.fromDirect(s.close())) - .map(r -> r.get("n").asNode().get("first_name").asString()).as(StepVerifier::create) - .expectNext("Freddie").verifyComplete(); + .map(r -> r.get("n").asNode().get("first_name").asString()) + .as(StepVerifier::create) + .expectNext("Freddie") + .verifyComplete(); } @Test void saveAll(@Autowired ReactivePersonRepository repository) { - Flux persons = repository.findById(id1).map(existingPerson -> { - existingPerson.setFirstName("Updated first name"); - existingPerson.setNullable("Updated nullable field"); - return existingPerson; - }).concatWith(Mono.fromSupplier(() -> { - PersonWithAllConstructor newPerson = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, - 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); - return newPerson; - })); + Flux persons = repository.findById(ReactiveRepositoryIT.this.id1) + .map(existingPerson -> { + existingPerson.setFirstName("Updated first name"); + existingPerson.setNullable("Updated nullable field"); + return existingPerson; + }) + .concatWith(Mono.fromSupplier(() -> { + PersonWithAllConstructor newPerson = new PersonWithAllConstructor(null, "Mercury", "Freddie", + "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); + return newPerson; + })); Flux operationUnderTest = repository.saveAll(persons).map(PersonWithAllConstructor::getId); List ids = new ArrayList<>(); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> ids) - .expectNextCount(2L).verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(2L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), - s -> Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", - Values.parameters("ids", ids))).flatMap(ReactiveResult::records), + s -> Flux.from( + s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", + Values.parameters("ids", ids))) + .flatMap(ReactiveResult::records), s -> Mono.fromDirect(s.close())) - .map(r -> r.get("n").asNode().get("name").asString()).as(StepVerifier::create) - .expectNext("Mercury").expectNext(TEST_PERSON1_NAME).verifyComplete(); + .map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("Mercury") + .expectNext(TEST_PERSON1_NAME) + .verifyComplete(); } @Test @@ -1706,50 +2107,73 @@ class ReactiveRepositoryIT { Flux operationUnderTest = repository.saveAll(List.of(newPerson)).map(PersonWithAllConstructor::getId); List ids = new ArrayList<>(); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).recordWith(() -> ids) - .expectNextCount(1L).verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), - s -> Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", - Values.parameters("ids", ids))).flatMap(ReactiveResult::records), - s -> Mono.fromDirect(s.close())).map(r -> r.get("n").asNode().get("name").asString()).as(StepVerifier::create) - .expectNext("Mercury").verifyComplete(); + s -> Flux.from( + s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", + Values.parameters("ids", ids))) + .flatMap(ReactiveResult::records), + s -> Mono.fromDirect(s.close())) + .map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("Mercury") + .verifyComplete(); } @Test void updateSingleEntity(@Autowired ReactivePersonRepository repository) { - Mono operationUnderTest = repository.findById(id1).map(originalPerson -> { - originalPerson.setFirstName("Updated first name"); - originalPerson.setNullable("Updated nullable field"); - return originalPerson; - }).flatMap(repository::save); + Mono operationUnderTest = repository.findById(ReactiveRepositoryIT.this.id1) + .map(originalPerson -> { + originalPerson.setFirstName("Updated first name"); + originalPerson.setNullable("Updated nullable field"); + return originalPerson; + }) + .flatMap(repository::save); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).expectNextCount(1L) - .verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), s -> { - Value parameters = Values.parameters("id", id1); - return Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters)).flatMap(ReactiveResult::records); - }, s -> Mono.fromDirect(s.close())).map(r -> r.get("n").asNode()).as(StepVerifier::create) - .expectNextMatches(node -> node.get("first_name").asString().equals("Updated first name") - && node.get("nullable").asString().equals("Updated nullable field")) - .verifyComplete(); + Value parameters = Values.parameters("id", ReactiveRepositoryIT.this.id1); + return Flux.from(s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters)) + .flatMap(ReactiveResult::records); + }, s -> Mono.fromDirect(s.close())) + .map(r -> r.get("n").asNode()) + .as(StepVerifier::create) + .expectNextMatches(node -> node.get("first_name").asString().equals("Updated first name") + && node.get("nullable").asString().equals("Updated nullable field")) + .verifyComplete(); } @Test void saveWithAssignedId(@Autowired ReactiveThingRepository repository) { - Mono operationUnderTest = Mono.fromSupplier(() -> new ThingWithAssignedId("aaBB", "That's the thing.")).flatMap(repository::save); + Mono operationUnderTest = Mono + .fromSupplier(() -> new ThingWithAssignedId("aaBB", "That's the thing.")) + .flatMap(repository::save); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).expectNextCount(1L) - .verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), - s -> Flux.from(s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", "aaBB"))).flatMap(ReactiveResult::records), - s -> Mono.fromDirect(s.close())) - .map(r -> r.get("n").asNode().get("name").asString()).as(StepVerifier::create) - .expectNext("That's the thing.").verifyComplete(); + s -> Flux + .from(s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", "aaBB"))) + .flatMap(ReactiveResult::records), + s -> Mono.fromDirect(s.close())) + .map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .verifyComplete(); repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } @@ -1764,14 +2188,23 @@ class ReactiveRepositoryIT { Flux operationUnderTest = repository.saveAll(things); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).expectNextCount(2L) - .verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), s -> { Value parameters = Values.parameters("ids", Arrays.asList("anId", "aaBB")); - return Flux.from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters)).flatMap(ReactiveResult::records); - }, s -> Mono.fromDirect(s.close())).map(r -> r.get("name").asString()).as(StepVerifier::create).expectNext("That's the thing.") - .expectNext("Updated name.").verifyComplete(); + return Flux + .from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters)) + .flatMap(ReactiveResult::records); + }, s -> Mono.fromDirect(s.close())) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .expectNext("Updated name.") + .verifyComplete(); // Make sure we triggered on insert, one update repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); @@ -1787,14 +2220,23 @@ class ReactiveRepositoryIT { Flux operationUnderTest = repository.saveAll(things); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).expectNextCount(2L) - .verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), s -> { Value parameters = Values.parameters("ids", Arrays.asList("anId", "aaBB")); - return Flux.from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters)).flatMap(ReactiveResult::records); - }, s -> Mono.fromDirect(s.close())).map(r -> r.get("name").asString()).as(StepVerifier::create).expectNext("That's the thing.") - .expectNext("Updated name.").verifyComplete(); + return Flux + .from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters)) + .flatMap(ReactiveResult::records); + }, s -> Mono.fromDirect(s.close())) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .expectNext("Updated name.") + .verifyComplete(); // Make sure we triggered on insert, one update repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); @@ -1805,7 +2247,8 @@ class ReactiveRepositoryIT { Flux operationUnderTest = Flux.concat( // Without prior selection - Mono.fromSupplier(() -> new ThingWithAssignedId("id07", "An updated thing")).flatMap(repository::save), + Mono.fromSupplier(() -> new ThingWithAssignedId("id07", "An updated thing")) + .flatMap(repository::save), // With prior selection repository.findById("id15").flatMap(thing -> { @@ -1813,14 +2256,22 @@ class ReactiveRepositoryIT { return repository.save(thing); })); - getTransactionalOperator().execute(t -> operationUnderTest).as(StepVerifier::create).expectNextCount(2L) - .verifyComplete(); + getTransactionalOperator().execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); Flux.usingWhen(Mono.fromSupplier(this::createRxSession), s -> { Value parameters = Values.parameters("ids", Arrays.asList("id07", "id15")); - return Flux.from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters)).flatMap(ReactiveResult::records); - }, s -> Mono.fromDirect(s.close())).map(r -> r.get("name").asString()).as(StepVerifier::create) - .expectNext("An updated thing", "Another updated thing").verifyComplete(); + return Flux + .from(s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters)) + .flatMap(ReactiveResult::records); + }, s -> Mono.fromDirect(s.close())) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("An updated thing", "Another updated thing") + .verifyComplete(); repository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); } @@ -1829,11 +2280,11 @@ class ReactiveRepositoryIT { void saveNewEntityWithGeneratedIdShouldNotIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId").consume()) - ); + doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .consume())); ThingWithFixedGeneratedId thing = new ThingWithFixedGeneratedId("name"); // this will create a duplicated relationship because we use the same ids @@ -1842,10 +2293,13 @@ class ReactiveRepositoryIT { // ensure that no relationship got deleted upfront assertInSession(session -> { - Long relCount = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") - .next().get("rCount").asLong()); + Long relCount = session + .executeRead(tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") + .next() + .get("rCount") + .asLong()); assertThat(relCount).isEqualTo(2); }); @@ -1855,20 +2309,28 @@ class ReactiveRepositoryIT { void updateEntityWithGeneratedIdShouldIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - Long rId = doWithSession(session -> session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()) - ); + Long rId = doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong())); - repository.findById("ThingWithFixedGeneratedId").flatMap(repository::save).as(StepVerifier::create) - .expectNextCount(1L).verifyComplete(); + repository.findById("ThingWithFixedGeneratedId") + .flatMap(repository::save) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertInSession(session -> { - Long newRid = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()); + Long newRid = session.executeRead( + tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong()); assertThat(rId).isNotEqualTo(newRid); }); @@ -1878,11 +2340,11 @@ class ReactiveRepositoryIT { void saveAllNewEntityWithGeneratedIdShouldNotIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - doWithSession(session -> - session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId").consume()) - ); + doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .consume())); ThingWithFixedGeneratedId thing = new ThingWithFixedGeneratedId("name"); // this will create a duplicated relationship because we use the same ids @@ -1891,10 +2353,13 @@ class ReactiveRepositoryIT { // ensure that no relationship got deleted upfront assertInSession(session -> { - Long relCount = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") - .next().get("rCount").asLong()); + Long relCount = session + .executeRead(tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return count(r) as rCount") + .next() + .get("rCount") + .asLong()); assertThat(relCount).isEqualTo(2); }); @@ -1904,22 +2369,26 @@ class ReactiveRepositoryIT { void updateAllEntityWithGeneratedIdShouldIssueRelationshipDeleteStatement( @Autowired ThingWithFixedGeneratedIdRepository repository) { - Long rId = doWithSession(session -> - session.executeWrite(tx -> - tx.run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()) - ); + Long rId = doWithSession(session -> session.executeWrite( + tx -> tx + .run("CREATE (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]->(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong())); repository.findById("ThingWithFixedGeneratedId") - .flatMap(loadedThing -> repository.saveAll(Collections.singletonList(loadedThing)).then()) - .block(); + .flatMap(loadedThing -> repository.saveAll(Collections.singletonList(loadedThing)).then()) + .block(); assertInSession(session -> { - Long newRid = session.executeRead(tx -> - tx.run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + - "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") - .next().get("rId").asLong()); + Long newRid = session.executeRead( + tx -> tx + .run("MATCH (:ThingWithFixedGeneratedId{theId:'ThingWithFixedGeneratedId'})" + + "-[r:KNOWS]-(:SimplePerson) return id(r) as rId") + .next() + .get("rId") + .asLong()); assertThat(rId).isNotEqualTo(newRid); }); @@ -1974,19 +2443,20 @@ class ReactiveRepositoryIT { List ids = new ArrayList<>(); getTransactionalOperator().execute(t -> repository.save(person).map(PersonWithRelationship::getId)) - .as(StepVerifier::create).recordWith(() -> ids).expectNextCount(1L).verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); assertInSession(session -> { - Record record = session.run( - """ + Record record = session.run(""" MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies, [(n)-[:Has]->(h:Hobby) | h] as hobbies, [(n)<-[:Has]-(c:Club) | c] as clubs - """, - Values.parameters("name", "Freddie")).single(); + """, Values.parameters("name", "Freddie")).single(); assertThat(record.containsKey("n")).isTrue(); Node rootNode = record.get("n").asNode(); @@ -2000,20 +2470,21 @@ class ReactiveRepositoryIT { pets.put(petWithHobbies.get(0), ((List) petWithHobbies.get(1))); } - assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("Jerry", "Tom"); + assertThat(pets.keySet() + .stream() + .map(pet -> ((Node) pet).get("name").asString()) + .collect(Collectors.toList())).containsExactlyInAnyOrder("Jerry", "Tom"); - assertThat(pets.values().stream() - .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("sleeping"); + assertThat(pets.values() + .stream() + .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())) + .collect(Collectors.toList())).containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); assertThat(record.get("clubs").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("ClownsClub"); + .containsExactlyInAnyOrder("ClownsClub"); }); } @@ -2038,20 +2509,25 @@ class ReactiveRepositoryIT { TransactionalOperator transactionalOperator = getTransactionalOperator(); transactionalOperator.execute(t -> repository.save(person).map(PersonWithRelationship::getId)) - .as(StepVerifier::create).recordWith(() -> ids).expectNextCount(1L).verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); - transactionalOperator.execute(t -> repository.save(person)).as(StepVerifier::create).expectNextCount(1L) - .verifyComplete(); + transactionalOperator.execute(t -> repository.save(person)) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertInSession(session -> { List recordList = session.run(""" - MATCH (n:PersonWithRelationship) - RETURN - n, - [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies, - [(n)-[:Has]->(h:Hobby) | h] as hobbies - """, Values.parameters("name", "Freddie")).list(); + MATCH (n:PersonWithRelationship) + RETURN + n, + [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies, + [(n)-[:Has]->(h:Hobby) | h] as hobbies + """, Values.parameters("name", "Freddie")).list(); // assert that there is only one record in the returned list assertThat(recordList).hasSize(1); @@ -2070,17 +2546,18 @@ class ReactiveRepositoryIT { pets.put(petWithHobbies.get(0), ((List) petWithHobbies.get(1))); } - assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("Jerry", "Tom"); + assertThat(pets.keySet() + .stream() + .map(pet -> ((Node) pet).get("name").asString()) + .collect(Collectors.toList())).containsExactlyInAnyOrder("Jerry", "Tom"); - assertThat(pets.values().stream() - .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect( - Collectors.toList())) - .containsExactlyInAnyOrder("sleeping"); + assertThat(pets.values() + .stream() + .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())) + .collect(Collectors.toList())).containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); // assert that only two hobbies is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); @@ -2095,7 +2572,10 @@ class ReactiveRepositoryIT { @Test void saveEntityWithAlreadyExistingTargetNode(@Autowired ReactiveRelationshipRepository repository) { - Long hobbyId = doWithSession(session -> session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId").single().get("hId").asLong()); + Long hobbyId = doWithSession(session -> session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId") + .single() + .get("hId") + .asLong()); PersonWithRelationship person = new PersonWithRelationship(); person.setName("Freddie"); @@ -2109,13 +2589,17 @@ class ReactiveRepositoryIT { TransactionalOperator transactionalOperator = getTransactionalOperator(); transactionalOperator.execute(t -> repository.save(person).map(PersonWithRelationship::getId)) - .as(StepVerifier::create).recordWith(() -> ids).expectNextCount(1L).verifyComplete(); + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); assertInSession(session -> { List recordList = session - .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", Values.parameters("name", "Freddie")) - .list(); + .run("MATCH (n:PersonWithRelationship) RETURN n, [(n)-[:Has]->(h:Hobby) | h] as hobbies", + Values.parameters("name", "Freddie")) + .list(); Record record = recordList.get(0); @@ -2125,7 +2609,7 @@ class ReactiveRepositoryIT { assertThat(rootNode.get("name").asString()).isEqualTo("Freddie"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) - .containsExactlyInAnyOrder("Music"); + .containsExactlyInAnyOrder("Music"); // assert that only one hobby is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); @@ -2147,10 +2631,13 @@ class ReactiveRepositoryIT { StepVerifier.create(repository.save(rootPet)).expectNextCount(1).verifyComplete(); assertInSession(session -> { - Record record = session.run(""" - MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)-[:Has]->(petOfGrandChildPet:Pet) - RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet - """, Collections.emptyMap()).single(); + Record record = session + .run(""" + MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)-[:Has]->(petOfGrandChildPet:Pet) + RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet + """, + Collections.emptyMap()) + .single(); assertThat(record.get("rootPet").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("petOfRootPet").asNode().get("name").asString()).isEqualTo("Daphne"); @@ -2169,7 +2656,9 @@ class ReactiveRepositoryIT { StepVerifier.create(repository.save(originalThing)).expectNextCount(1).verifyComplete(); assertInSession(session -> { - Record record = session.run("MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'}) RETURN r").single(); + Record record = session.run( + "MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'}) RETURN r") + .single(); assertThat(record.keys()).isNotEmpty(); assertThat(record.containsKey("r")).isTrue(); @@ -2202,7 +2691,9 @@ class ReactiveRepositoryIT { StepVerifier.create(repository.save(luna)).expectNextCount(1).verifyComplete(); assertInSession(session -> { - Record record = session.run("MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(luna2:Pet{name:'Luna'})RETURN luna, daphne, luna2").single(); + Record record = session.run( + "MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})-[:Has]->(luna2:Pet{name:'Luna'})RETURN luna, daphne, luna2") + .single(); assertThat(record.get("luna").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("daphne").asNode().get("name").asString()).isEqualTo("Daphne"); @@ -2221,15 +2712,18 @@ class ReactiveRepositoryIT { StepVerifier.create(repository.save(start)).expectNextCount(1).verifyComplete(); assertInSession(session -> { - List records = session.run("MATCH (end:BidirectionalEnd)<-[r:CONNECTED]-(start:BidirectionalStart)" + - " RETURN start, r, end").list(); + List records = session + .run("MATCH (end:BidirectionalEnd)<-[r:CONNECTED]-(start:BidirectionalStart)" + + " RETURN start, r, end") + .list(); assertThat(records).hasSize(1); }); } @Test // GH-2196 - void saveSameNodeWithDoubleRelationship(@Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) { + void saveSameNodeWithDoubleRelationship( + @Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) { AltHobby hobby = new AltHobby(); hobby.setName("Music"); @@ -2245,40 +2739,33 @@ class ReactiveRepositoryIT { hobby.getLikedBy().add(rel1); hobby.getLikedBy().add(rel2); - StepVerifier.create(repository.save(hobby)) - .expectNextCount(1) - .verifyComplete(); + StepVerifier.create(repository.save(hobby)).expectNextCount(1).verifyComplete(); - StepVerifier.create(repository.loadFromCustomQuery(altPerson.getId())) - .assertNext(loadedHobby -> { - assertThat(loadedHobby.getName()).isEqualTo("Music"); - List likedBy = loadedHobby.getLikedBy(); - assertThat(likedBy).hasSize(2); - assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2); - }) - .verifyComplete(); + StepVerifier.create(repository.loadFromCustomQuery(altPerson.getId())).assertNext(loadedHobby -> { + assertThat(loadedHobby.getName()).isEqualTo("Music"); + List likedBy = loadedHobby.getLikedBy(); + assertThat(likedBy).hasSize(2); + assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2); + }).verifyComplete(); } @Test // GH-2240 - void saveBidirectionalRelationshipsWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) { + void saveBidirectionalRelationshipsWithExternallyGeneratedId( + @Autowired BidirectionalExternallyGeneratedIdRepository repository) { BidirectionalExternallyGeneratedId a = new BidirectionalExternallyGeneratedId(); - StepVerifier.create( - repository.save(a).flatMap(savedA -> { - BidirectionalExternallyGeneratedId b = new BidirectionalExternallyGeneratedId(); - b.otter = savedA; - savedA.otter = b; - return repository.save(b); - }) - ) - .assertNext(savedB -> { + StepVerifier.create(repository.save(a).flatMap(savedA -> { + BidirectionalExternallyGeneratedId b = new BidirectionalExternallyGeneratedId(); + b.otter = savedA; + savedA.otter = b; + return repository.save(b); + })).assertNext(savedB -> { assertThat(savedB.uuid).isNotNull(); assertThat(savedB.otter).isNotNull(); assertThat(savedB.otter.uuid).isNotNull(); // this would be b again assertThat(savedB.otter.otter).isNotNull(); - }) - .verifyComplete(); + }).verifyComplete(); } @@ -2288,25 +2775,22 @@ class ReactiveRepositoryIT { BidirectionalAssignedId a = new BidirectionalAssignedId(); a.uuid = UUID.randomUUID(); - StepVerifier.create( - repository.save(a).flatMap(savedA -> { - BidirectionalAssignedId b = new BidirectionalAssignedId(); - b.uuid = UUID.randomUUID(); - b.otter = savedA; - savedA.otter = b; - return repository.save(b); - }) - ) - .assertNext(savedB -> { - assertThat(savedB.uuid).isNotNull(); - assertThat(savedB.otter).isNotNull(); - assertThat(savedB.otter.uuid).isNotNull(); - // this would be b again - assertThat(savedB.otter.otter).isNotNull(); - }) - .verifyComplete(); + StepVerifier.create(repository.save(a).flatMap(savedA -> { + BidirectionalAssignedId b = new BidirectionalAssignedId(); + b.uuid = UUID.randomUUID(); + b.otter = savedA; + savedA.otter = b; + return repository.save(b); + })).assertNext(savedB -> { + assertThat(savedB.uuid).isNotNull(); + assertThat(savedB.otter).isNotNull(); + assertThat(savedB.otter.uuid).isNotNull(); + // this would be b again + assertThat(savedB.otter.otter).isNotNull(); + }).verifyComplete(); } + } @Nested @@ -2317,27 +2801,35 @@ class ReactiveRepositoryIT { transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ)) - .next().get(0).asLong(); + ReactiveRepositoryIT.this.id1 = transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) + .next() + .get(0) + .asLong(); - id2 = transaction.run( + ReactiveRepositoryIT.this.id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) - .next().get(0).asLong(); + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, + "place", SFO)) + .next() + .get(0) + .asLong(); - person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); + ReactiveRepositoryIT.this.person1 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id1, + TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, + "something", Arrays.asList("a", "b"), NEO4J_HQ, null); - person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + ReactiveRepositoryIT.this.person2 = new PersonWithAllConstructor(ReactiveRepositoryIT.this.id2, + TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, + null, Collections.emptyList(), SFO, null); } @Test @@ -2349,55 +2841,82 @@ class ReactiveRepositoryIT { @Test void deleteById(@Autowired ReactivePersonRepository repository) { - repository.deleteById(id1).then(repository.existsById(id1)).concatWith(repository.existsById(id2)) - .as(StepVerifier::create).expectNext(false, true).verifyComplete(); + repository.deleteById(ReactiveRepositoryIT.this.id1) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); } @Test void deleteByIdPublisher(@Autowired ReactivePersonRepository repository) { - repository.deleteById(Mono.just(id1)).then(repository.existsById(id1)).concatWith(repository.existsById(id2)) - .as(StepVerifier::create).expectNext(false, true).verifyComplete(); + repository.deleteById(Mono.just(ReactiveRepositoryIT.this.id1)) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); } @Test void delete(@Autowired ReactivePersonRepository repository) { - repository.delete(person1).then(repository.existsById(id1)).concatWith(repository.existsById(id2)) - .as(StepVerifier::create).expectNext(false, true).verifyComplete(); + repository.delete(ReactiveRepositoryIT.this.person1) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); } @Test void deleteAllEntities(@Autowired ReactivePersonRepository repository) { - repository.deleteAll(Arrays.asList(person1, person2)).then(repository.existsById(id1)) - .concatWith(repository.existsById(id2)).as(StepVerifier::create).expectNext(false, false).verifyComplete(); + repository.deleteAll(Arrays.asList(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2)) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, false) + .verifyComplete(); } @Test void deleteAllEntitiesPublisher(@Autowired ReactivePersonRepository repository) { - repository.deleteAll(Flux.just(person1, person2)).then(repository.existsById(id1)) - .concatWith(repository.existsById(id2)).as(StepVerifier::create).expectNext(false, false).verifyComplete(); + repository.deleteAll(Flux.just(ReactiveRepositoryIT.this.person1, ReactiveRepositoryIT.this.person2)) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, false) + .verifyComplete(); } @Test // DATAGRAPH-1428 void deleteAllById(@Autowired ReactivePersonRepository repository) { - repository.deleteAllById(Arrays.asList(person1.getId(), person2.getId())).then(repository.existsById(id1)) - .concatWith(repository.existsById(id2)).as(StepVerifier::create).expectNext(false, false).verifyComplete(); + repository + .deleteAllById(Arrays.asList(ReactiveRepositoryIT.this.person1.getId(), + ReactiveRepositoryIT.this.person2.getId())) + .then(repository.existsById(ReactiveRepositoryIT.this.id1)) + .concatWith(repository.existsById(ReactiveRepositoryIT.this.id2)) + .as(StepVerifier::create) + .expectNext(false, false) + .verifyComplete(); } @Test void deleteSimpleRelationship(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})").consume() - ); + doWithSession(session -> session + .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})") + .consume()); - Publisher personLoad = repository.getPersonWithRelationshipsViaQuery().map(person -> { - person.setHobbies(null); - return person; - }); + Publisher personLoad = repository.getPersonWithRelationshipsViaQuery() + .map(person -> { + person.setHobbies(null); + return person; + }); Flux personSave = repository.saveAll(personLoad); @@ -2408,15 +2927,14 @@ class ReactiveRepositoryIT { @Test void deleteCollectionRelationship(@Autowired ReactiveRelationshipRepository repository) { - doWithSession(session -> - session.run("CREATE (n:PersonWithRelationship{name:'Freddie'}), " - + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})") - ); + doWithSession(session -> session.run("CREATE (n:PersonWithRelationship{name:'Freddie'}), " + + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})")); - Publisher personLoad = repository.getPersonWithRelationshipsViaQuery().map(person -> { - person.getPets().remove(0); - return person; - }); + Publisher personLoad = repository.getPersonWithRelationshipsViaQuery() + .map(person -> { + person.getPets().remove(0); + return person; + }); Flux personSave = repository.saveAll(personLoad); @@ -2428,25 +2946,36 @@ class ReactiveRepositoryIT { @Test // GH-2281 void deleteByDerivedQuery1(@Autowired ReactivePersonRepository repository) { - repository.deleteAllByName(TEST_PERSON1_NAME) - .as(StepVerifier::create) - .verifyComplete(); + repository.deleteAllByName(TEST_PERSON1_NAME).as(StepVerifier::create).verifyComplete(); - repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete(); - repository.existsById(id2).as(StepVerifier::create).expectNext(true).verifyComplete(); + repository.existsById(ReactiveRepositoryIT.this.id1) + .as(StepVerifier::create) + .expectNext(false) + .verifyComplete(); + repository.existsById(ReactiveRepositoryIT.this.id2) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); } @Test // GH-2281 void deleteByDerivedQuery2(@Autowired ReactivePersonRepository repository) { repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) - .as(StepVerifier::create) - .expectNext(2L) - .verifyComplete(); + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); - repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete(); - repository.existsById(id2).as(StepVerifier::create).expectNext(false).verifyComplete(); + repository.existsById(ReactiveRepositoryIT.this.id1) + .as(StepVerifier::create) + .expectNext(false) + .verifyComplete(); + repository.existsById(ReactiveRepositoryIT.this.id2) + .as(StepVerifier::create) + .expectNext(false) + .verifyComplete(); } + } @Nested @@ -2457,38 +2986,47 @@ class ReactiveRepositoryIT { transaction.run("MATCH (n) detach delete n"); - transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place\s - RETURN id(n) - """, - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", - NEO4J_HQ) - ).next().get(0).asLong(); + transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place\s + RETURN id(n) + """, + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", + TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) + .next() + .get(0) + .asLong(); - transaction.run(""" - CREATE (n:PersonWithAllConstructor) - SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place - return id(n) - """, - Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO) - ).next().get(0).asLong(); + transaction + .run(""" + CREATE (n:PersonWithAllConstructor) + SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place + return id(n) + """, + Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", + TEST_PERSON2_BORN_ON, "place", SFO)) + .next() + .get(0) + .asLong(); } @Test void mapsInterfaceProjectionWithDerivedFinderMethod(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByName(TEST_PERSON1_NAME)) - .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) - .verifyComplete(); + .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) + .verifyComplete(); } @Test void mapsDtoProjectionWithDerivedFinderMethod(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)).expectNextCount(1).verifyComplete(); + StepVerifier.create(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)) + .expectNextCount(1) + .verifyComplete(); } @Test @@ -2502,8 +3040,8 @@ class ReactiveRepositoryIT { void mapsInterfaceProjectionWithCustomQueryAndMapProjection(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByNameWithCustomQueryAndMapProjection(TEST_PERSON1_NAME)) - .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) - .verifyComplete(); + .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) + .verifyComplete(); } @Test @@ -2517,8 +3055,8 @@ class ReactiveRepositoryIT { void mapsInterfaceProjectionWithCustomQueryAndNodeReturn(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByNameWithCustomQueryAndNodeReturn(TEST_PERSON1_NAME)) - .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) - .verifyComplete(); + .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) + .verifyComplete(); } @Test @@ -2531,13 +3069,15 @@ class ReactiveRepositoryIT { @Test // DATAGRAPH-1438 void mapsOptionalDtoProjectionWithDerivedFinderMethod(@Autowired ReactivePersonRepository repository) { - StepVerifier.create(repository.findOneByFirstName(TEST_PERSON1_FIRST_NAME).map(DtoPersonProjection::getFirstName)) - .expectNext(TEST_PERSON1_FIRST_NAME) - .verifyComplete(); + StepVerifier + .create(repository.findOneByFirstName(TEST_PERSON1_FIRST_NAME).map(DtoPersonProjection::getFirstName)) + .expectNext(TEST_PERSON1_FIRST_NAME) + .verifyComplete(); StepVerifier.create(repository.findOneByFirstName("foobar").map(DtoPersonProjection::getFirstName)) - .verifyComplete(); + .verifyComplete(); } + } @Nested @@ -2555,7 +3095,9 @@ class ReactiveRepositoryIT { @Test void createAllNodesWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository repository) { - repository.saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntity())).collectList().block(); + repository.saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntity())) + .collectList() + .block(); assertInSession(session -> { Node node = session.run("MATCH (n:A) return n").single().get("n").asNode(); @@ -2586,7 +3128,8 @@ class ReactiveRepositoryIT { long n2Id; long n3Id; - Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); + Record record = doWithSession( + session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode()); n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode()); n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode()); @@ -2603,7 +3146,8 @@ class ReactiveRepositoryIT { long n2Id; long n3Id; - Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); + Record record = doWithSession( + session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single()); n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode()); n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode()); n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode()); @@ -2634,8 +3178,9 @@ class ReactiveRepositoryIT { @Test void createAllNodesWithMultipleLabels(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { - repository.saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L))).collectList() - .block(); + repository.saveAll(Collections.singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L))) + .collectList() + .block(); assertInSession(session -> { Node node = session.run("MATCH (n:X) return n").single().get("n").asNode(); @@ -2666,10 +3211,8 @@ class ReactiveRepositoryIT { void createNodeWithCustomIdAndDynamicLabels( @Autowired EntityWithCustomIdAndDynamicLabelsRepository repository) { - EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity1 - = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); - EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity2 - = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity1 = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels entity2 = new EntitiesWithDynamicLabels.EntityWithCustomIdAndDynamicLabels(); entity1.identifier = "id1"; entity1.myLabels = Collections.singleton("LabelEntity1"); @@ -2685,10 +3228,9 @@ class ReactiveRepositoryIT { assertInSession(session -> { List result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity1) return e") - .list(); + .list(); assertThat(result).hasSize(1); - result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity2) return e") - .list(); + result = session.run("MATCH (e:EntityWithCustomIdAndDynamicLabels:LabelEntity2) return e").list(); assertThat(result).hasSize(1); }); } @@ -2700,8 +3242,9 @@ class ReactiveRepositoryIT { long n2Id; long n3Id; - Record record = doWithSession(session -> session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") - .single()); + Record record = doWithSession(session -> session + .run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") + .single()); n1Id = record.get("n1").asNode().get("id").asLong(); n2Id = record.get("n2").asNode().get("id").asLong(); n3Id = record.get("n3").asNode().get("id").asLong(); @@ -2718,8 +3261,9 @@ class ReactiveRepositoryIT { long n2Id; long n3Id; - Record record = doWithSession(session -> session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") - .single()); + Record record = doWithSession(session -> session + .run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3") + .single()); n1Id = record.get("n1").asNode().get("id").asLong(); n2Id = record.get("n2").asNode().get("id").asLong(); n3Id = record.get("n3").asNode().get("id").asLong(); @@ -2734,202 +3278,7 @@ class ReactiveRepositoryIT { assertThat(session.run("MATCH (n:X) return n").list()).hasSize(1); }); } + } - interface BidirectionalExternallyGeneratedIdRepository - extends ReactiveNeo4jRepository {} - - interface BidirectionalAssignedIdRepository - extends ReactiveNeo4jRepository {} - - interface BidirectionalStartRepository extends ReactiveNeo4jRepository {} - - interface BidirectionalEndRepository extends ReactiveNeo4jRepository {} - - interface ImmutablePersonRepository extends ReactiveNeo4jRepository {} - - interface ReactiveLoopingRelationshipRepository - extends ReactiveNeo4jRepository {} - - interface ReactiveMultipleLabelRepository - extends ReactiveNeo4jRepository {} - - interface ReactiveMultipleLabelWithAssignedIdRepository - extends ReactiveNeo4jRepository {} - - interface ReactivePersonWithRelationshipWithPropertiesRepository - extends ReactiveNeo4jRepository { - - @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") - Mono loadFromCustomQuery(@Param("id") Long id); - - Mono findByHobbiesSince(int since); - - Mono findByHobbiesSinceOrHobbiesActive(int since1, boolean active); - - Mono findByHobbiesSinceAndHobbiesActive(int since1, boolean active); - - @Query("MATCH (p:PersonWithRelationshipWithProperties) return p {.name}") - Mono justTheNames(); - } - - interface ReactiveHobbyWithRelationshipWithPropertiesRepository - extends ReactiveNeo4jRepository { - - @Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)") - Flux loadFromCustomQuery(@Param("personId") Long personId); - } - - interface ReactivePetRepository extends ReactiveNeo4jRepository { - Mono countByName(String name); - - Mono existsByName(String name); - - Mono countByFriendsNameAndFriendsFriendsName(String friendName, String friendFriendName); - } - - interface ReactiveRelationshipRepository extends ReactiveNeo4jRepository { - - @Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) " - + "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets " - + "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) " - + "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies") - Mono getPersonWithRelationshipsViaQuery(); - - Mono findByPetsName(String petName); - - Mono findByHobbiesNameOrPetsName(String hobbyName, String petName); - - Mono findByHobbiesNameAndPetsName(String hobbyName, String petName); - - Mono findByPetsHobbiesName(String hobbyName); - - Mono findByPetsFriendsName(String petName); - - Flux findByName(String name, Sort sort); - - Mono findDistinctByHobbiesName(String hobbyName); - } - - interface ReactiveSimilarThingRepository extends ReactiveCrudRepository {} - - interface EntityWithConvertedIdRepository - extends ReactiveNeo4jRepository {} - - interface ThingWithFixedGeneratedIdRepository extends ReactiveNeo4jRepository {} - - interface EntityWithCustomIdAndDynamicLabelsRepository - extends ReactiveNeo4jRepository {} - - @SpringJUnitConfig(ReactiveRepositoryIT.Config.class) - static abstract class ReactiveIntegrationTestBase { - - @Autowired private Driver driver; - - @Autowired private TransactionalOperator transactionalOperator; - - @Autowired private BookmarkCapture bookmarkCapture; - - void setupData(TransactionContext transaction) { - } - - @BeforeEach - void before() { - doWithSession(session -> - session.executeWrite(tx -> { - tx.run("MATCH (n) detach delete n").consume(); - setupData(tx); - return null; - })); - } - - T doWithSession(Function sessionConsumer) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { - T result = sessionConsumer.apply(session); - bookmarkCapture.seedWith(session.lastBookmarks()); - return result; - } - } - - void assertInSession(Consumer consumer) { - - try (Session session = driver.session(bookmarkCapture.createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue()))) { - consumer.accept(session); - } - } - - ReactiveSession createRxSession() { - - return driver.session(ReactiveSession.class, bookmarkCapture.createSessionConfig(databaseSelection.get().getValue(), userSelection.get().getValue())); - } - - TransactionalOperator getTransactionalOperator() { - return transactionalOperator; - } - } - - @Configuration - @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) - @EnableTransactionManagement - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Override - public Collection getMappingBasePackages() { - return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - return ReactiveNeo4jTransactionManager.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .withBookmarkManager(Neo4jBookmarkManager.createReactive(bookmarkCapture())) - .build(); - } - - @Override - public ReactiveNeo4jClient neo4jClient(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - return ReactiveNeo4jClient.with(driver) - .withDatabaseSelectionProvider(databaseSelectionProvider) - .withUserSelectionProvider(getUserSelectionProvider()) - .build(); - } - - @Bean - public TransactionalOperator transactionalOperator(ReactiveTransactionManager reactiveTransactionManager) { - return TransactionalOperator.create(reactiveTransactionManager); - } - - @Override - @Bean - public ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider() { - return Optional.ofNullable(databaseSelection.get().getValue()) // The thread local must be resolved early, before the mono - .map(ReactiveDatabaseSelectionProvider::createStaticDatabaseSelectionProvider) - .orElse(ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider()); - } - - @Bean - public ReactiveUserSelectionProvider getUserSelectionProvider() { - return Optional.ofNullable(userSelection.get()) // The thread local must be resolved early, before the mono - .map(u -> (ReactiveUserSelectionProvider) () -> Mono.just(u)) - .orElse(ReactiveUserSelectionProvider.getDefaultSelectionProvider()); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java index cfc01c5b8..bf9b32aa0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; + import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.test.Neo4jExtension; @@ -54,4 +55,5 @@ class ReactiveRepositoryWithADifferentDatabaseIT extends ReactiveRepositoryIT { databaseSelection.set(DatabaseSelection.undecided()); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentUserIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentUserIT.java index a719fc73c..316b71d3b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentUserIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentUserIT.java @@ -15,18 +15,19 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assumptions.assumeThat; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -36,6 +37,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; class ReactiveRepositoryWithADifferentUserIT extends ReactiveRepositoryIT { private static final String TEST_USER = "sdn62"; + private static final String TEST_DATABASE_NAME = "sdn62db"; @BeforeAll @@ -46,12 +48,13 @@ class ReactiveRepositoryWithADifferentUserIT extends ReactiveRepositoryIT { try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) { session.run("CREATE DATABASE $db", Values.parameters("db", TEST_DATABASE_NAME)).consume(); - session.run("CREATE USER $user SET PASSWORD $password CHANGE NOT REQUIRED SET HOME DATABASE $database", - Values.parameters("user", TEST_USER, "password", TEST_USER + "_password", "database", TEST_DATABASE_NAME)) - .consume(); + session + .run("CREATE USER $user SET PASSWORD $password CHANGE NOT REQUIRED SET HOME DATABASE $database", Values + .parameters("user", TEST_USER, "password", TEST_USER + "_password", "database", TEST_DATABASE_NAME)) + .consume(); session.run("GRANT ROLE publisher TO $user", Values.parameters("user", TEST_USER)).consume(); session.run("GRANT IMPERSONATE ($targetUser) ON DBMS TO admin", Values.parameters("targetUser", TEST_USER)) - .consume(); + .consume(); } userSelection.set(UserSelection.impersonate(TEST_USER)); @@ -62,12 +65,14 @@ class ReactiveRepositoryWithADifferentUserIT extends ReactiveRepositoryIT { try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) { - session.run("REVOKE IMPERSONATE ($targetUser) ON DBMS FROM admin", - Values.parameters("targetUser", TEST_USER)).consume(); + session + .run("REVOKE IMPERSONATE ($targetUser) ON DBMS FROM admin", Values.parameters("targetUser", TEST_USER)) + .consume(); session.run("DROP USER $user", Values.parameters("user", TEST_USER)).consume(); session.run("DROP DATABASE $db", Values.parameters("db", TEST_DATABASE_NAME)).consume(); } userSelection.set(UserSelection.connectedUser()); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java index 6c0553e66..9e1d6af06 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -26,13 +24,15 @@ import org.assertj.core.data.Index; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.Nested; import org.neo4j.driver.Driver; import org.neo4j.driver.Values; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -56,7 +56,7 @@ import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.ReactiveTransactionManager; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -68,6 +68,38 @@ class ReactiveScrollingIT { @SuppressWarnings("unused") private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + @Configuration + @EnableNeo4jRepositories + @EnableReactiveNeo4jRepositories + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + @Nested @SpringJUnitConfig(Config.class) @DisplayName("Scroll with derived finder method") @@ -75,10 +107,8 @@ class ReactiveScrollingIT { @BeforeAll static void setupTestData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { - try ( - var session = driver.session(bookmarkCapture.createSessionConfig()); - var transaction = session.beginTransaction() - ) { + try (var session = driver.session(bookmarkCapture.createSessionConfig()); + var transaction = session.beginTransaction()) { ScrollingEntity.createTestData(transaction); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -90,10 +120,10 @@ class ReactiveScrollingIT { void oneColumnSortNoScroll(@Autowired ReactiveScrollingRepository repository) { repository.findTop4ByOrderByB() - .map(ScrollingEntity::getA) - .as(StepVerifier::create) - .expectNext("A0", "B0", "C0", "D0") - .verifyComplete(); + .map(ScrollingEntity::getA) + .as(StepVerifier::create) + .expectNext("A0", "B0", "C0", "D0") + .verifyComplete(); } @Order(2) @@ -101,46 +131,44 @@ class ReactiveScrollingIT { void forwardWithDuplicatesManualIteration(@Autowired ReactiveScrollingRepository repository) { var duplicates = new ArrayList(); - repository.findAllByAOrderById("D0").as(StepVerifier::create) - .recordWith(() -> duplicates) - .expectNextCount(2) - .verifyComplete(); + repository.findAllByAOrderById("D0") + .as(StepVerifier::create) + .recordWith(() -> duplicates) + .expectNextCount(2) + .verifyComplete(); var windowContainer = new AtomicReference>(); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, ScrollPosition.keyset()) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); var window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(0).getId()), Index.atIndex(3)) - .extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0", "C0", "D0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(0).getId()), Index.atIndex(3)) + .extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0", "C0", "D0"); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, window.positionAt(window.size() - 1)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(1).getId()), Index.atIndex(0)) - .extracting(ScrollingEntity::getA) - .containsExactly("D0", "E0", "F0", "G0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(1).getId()), Index.atIndex(0)) + .extracting(ScrollingEntity::getA) + .containsExactly("D0", "E0", "F0", "G0"); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, window.positionAt(window.size() - 1)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("H0", "I0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("H0", "I0"); } @Test @@ -149,55 +177,49 @@ class ReactiveScrollingIT { // Recreate the last position var last = repository.findFirstByA("I0").block(); - var keys = Map.of( - "foobar", Values.value(last.getA()), - "b", Values.value(last.getB()), - Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) - ); + var keys = Map.of("foobar", Values.value(last.getA()), "b", Values.value(last.getB()), + Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString())); var duplicates = new ArrayList(); - repository.findAllByAOrderById("D0").as(StepVerifier::create) - .recordWith(() -> duplicates) - .expectNextCount(2) - .verifyComplete(); + repository.findAllByAOrderById("D0") + .as(StepVerifier::create) + .recordWith(() -> duplicates) + .expectNextCount(2) + .verifyComplete(); var windowContainer = new AtomicReference>(); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, ScrollPosition.backward(keys)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); var window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("F0", "G0", "H0", "I0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("F0", "G0", "H0", "I0"); var pos = ((KeysetScrollPosition) window.positionAt(0)); pos = ScrollPosition.backward(pos.getKeys()); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, pos) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .extracting(ScrollingEntity::getA) - .containsExactly("C0", "D0", "D0", "E0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("C0", "D0", "D0", "E0"); pos = ((KeysetScrollPosition) window.positionAt(0)); pos = ScrollPosition.backward(pos.getKeys()); repository.findTop4By(ScrollingEntity.SORT_BY_B_AND_A, pos) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("A0", "B0"); } + } @Nested @@ -207,10 +229,8 @@ class ReactiveScrollingIT { @BeforeAll static void setupTestData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { - try ( - var session = driver.session(bookmarkCapture.createSessionConfig()); - var transaction = session.beginTransaction() - ) { + try (var session = driver.session(bookmarkCapture.createSessionConfig()); + var transaction = session.beginTransaction()) { ScrollingEntity.createTestDataWithoutDuplicates(transaction); transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); @@ -222,41 +242,36 @@ class ReactiveScrollingIT { @Tag("GH-2726") void forwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) { ScrollingEntity scrollingEntity = new ScrollingEntity(); - Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example.of(scrollingEntity, + ExampleMatcher.matchingAll().withIgnoreNullValues()); var windowContainer = new AtomicReference>(); - repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + repository + .findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); var window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("A0", "B0", "C0", "D0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("A0", "B0", "C0", "D0"); ScrollPosition nextScrollPosition = window.positionAt(window.size() - 1); repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextScrollPosition)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("E0", "F0", "G0", "H0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("E0", "F0", "G0", "H0"); ScrollPosition nextNextScrollPosition = window.positionAt(window.size() - 1); repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextScrollPosition)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("I0"); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("I0"); } @Test @@ -264,78 +279,47 @@ class ReactiveScrollingIT { @Tag("GH-2726") void backwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) { - Example example = Example.of(new ScrollingEntity(), ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example.of(new ScrollingEntity(), + ExampleMatcher.matchingAll().withIgnoreNullValues()); // Recreate the last position var last = repository.findFirstByA("I0").block(); - var keys = Map.of( - "c", Values.value(last.getC()), - Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) - ); + var keys = Map.of("c", Values.value(last.getC()), Constants.NAME_OF_ADDITIONAL_SORT, + Values.value(last.getId().toString())); var windowContainer = new AtomicReference>(); - repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + repository + .findBy(example, + q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); var window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(ScrollingEntity::getA) - .containsExactly("F0", "G0", "H0", "I0"); + assertThat(window).hasSize(4).extracting(ScrollingEntity::getA).containsExactly("F0", "G0", "H0", "I0"); var nextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.hasNext()).isTrue(); - assertThat(window) - .hasSize(4) - .extracting(Function.identity()) - .extracting(ScrollingEntity::getA) - .containsExactly("B0", "C0", "D0", "E0"); + assertThat(window).hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("B0", "C0", "D0", "E0"); var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos)) - .as(StepVerifier::create) - .consumeNextWith(windowContainer::set) - .verifyComplete(); + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); window = windowContainer.get(); assertThat(window.isLast()).isTrue(); - assertThat(window).extracting(ScrollingEntity::getA) - .containsExactly("A0"); - } - } - - @Configuration - @EnableNeo4jRepositories - @EnableReactiveNeo4jRepositories - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + assertThat(window).extracting(ScrollingEntity::getA).containsExactly("A0"); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java index 1881e9ebb..ea8c18c2f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java @@ -15,20 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; - -import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; -import org.springframework.data.neo4j.integration.shared.common.Club; -import org.springframework.data.neo4j.integration.shared.common.ClubRelationship; -import org.springframework.data.neo4j.integration.shared.common.Hobby; -import org.springframework.data.neo4j.integration.shared.common.HobbyRelationship; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.test.StepVerifier; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -39,10 +25,19 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.Club; +import org.springframework.data.neo4j.integration.shared.common.ClubRelationship; import org.springframework.data.neo4j.integration.shared.common.DynamicRelationshipsITBase; +import org.springframework.data.neo4j.integration.shared.common.Hobby; +import org.springframework.data.neo4j.integration.shared.common.HobbyRelationship; import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.integration.shared.common.PersonWithStringlyTypedRelatives; import org.springframework.data.neo4j.integration.shared.common.Pet; @@ -50,10 +45,14 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons */ @@ -68,7 +67,7 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB @Test void shouldReadDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { + repository.findById(this.idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); @@ -87,7 +86,7 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB @Test // GH-216 void shouldReadDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { + repository.findById(this.idOfExistingPerson).as(StepVerifier::create).consumeNextWith(person -> { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); @@ -99,14 +98,15 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB Map> hobbies = person.getHobbies(); assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getPerformance).containsExactly("average"); assertThat(hobbies.get("ACTIVE")).extracting(HobbyRelationship::getHobby) - .extracting(Hobby::getName).containsExactly("Biking"); + .extracting(Hobby::getName) + .containsExactly("Biking"); }).verifyComplete(); } @Test // DATAGRAPH-1449 void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).map(person -> { + repository.findById(this.idOfExistingPerson).map(person -> { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -128,26 +128,30 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB clubs.put("BASEBALL", clubRelationship); return person; - }).flatMap(repository::save) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create).consumeNextWith(person -> { - Map relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); - assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); - assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); + }) + .flatMap(repository::save) + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); + assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); + assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); - Map clubs = person.getClubs(); - assertThat(clubs).containsOnlyKeys("BASEBALL"); - assertThat(clubs.get("BASEBALL")).extracting(ClubRelationship::getPlace).isEqualTo("Boston"); - assertThat(clubs.get("BASEBALL")).extracting(ClubRelationship::getClub) - .extracting(Club::getName).isEqualTo("Red Sox"); - }).verifyComplete(); + Map clubs = person.getClubs(); + assertThat(clubs).containsOnlyKeys("BASEBALL"); + assertThat(clubs.get("BASEBALL")).extracting(ClubRelationship::getPlace).isEqualTo("Boston"); + assertThat(clubs.get("BASEBALL")).extracting(ClubRelationship::getClub) + .extracting(Club::getName) + .isEqualTo("Red Sox"); + }) + .verifyComplete(); } @Test // GH-216 // DATAGRAPH-1449 void shouldUpdateDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { - repository.findById(idOfExistingPerson).map(person -> { + repository.findById(this.idOfExistingPerson).map(person -> { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); @@ -169,21 +173,26 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB hobbies.put("WATCHING", Collections.singletonList(hobbyRelationship)); return person; - }).flatMap(repository::save) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create).consumeNextWith(person -> { - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "FISH"); - assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); - assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + }) + .flatMap(repository::save) + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "FISH"); + assertThat(pets.get("CATS")).extracting(Pet::getName) + .containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); - Map> hobbies = person.getHobbies(); - assertThat(hobbies).containsOnlyKeys("WATCHING"); - assertThat(hobbies.get("WATCHING")).extracting(HobbyRelationship::getPerformance) + Map> hobbies = person.getHobbies(); + assertThat(hobbies).containsOnlyKeys("WATCHING"); + assertThat(hobbies.get("WATCHING")).extracting(HobbyRelationship::getPerformance) .containsExactly("average"); - assertThat(hobbies.get("WATCHING")).extracting(HobbyRelationship::getHobby) - .extracting(Hobby::getName).containsExactly("Football"); - }).verifyComplete(); + assertThat(hobbies.get("WATCHING")).extracting(HobbyRelationship::getHobby) + .extracting(Hobby::getName) + .containsExactly("Football"); + }) + .verifyComplete(); } @Test // DATAGRAPH-1447 @@ -212,21 +221,30 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB List recorded = new ArrayList<>(); repository.save(newPerson) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create).recordWith(() -> recorded) - .consumeNextWith(personWithRelatives -> { - Map relatives = personWithRelatives.getRelatives(); - assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); - }).verifyComplete(); + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(personWithRelatives -> { + Map relatives = personWithRelatives.getRelatives(); + assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); + }) + .verifyComplete(); - try (Transaction transaction = driver.session(bookmarkCapture.createSessionConfig()).beginTransaction()) { + try (Transaction transaction = this.driver.session(this.bookmarkCapture.createSessionConfig()) + .beginTransaction()) { long numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Person) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Club) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } @@ -260,27 +278,37 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB List recorded = new ArrayList<>(); repository.save(newPerson) - .flatMap(p -> repository.findById(p.getId())) - .as(StepVerifier::create).recordWith(() -> recorded).consumeNextWith(person -> { - Map> writtenPets = person.getPets(); - assertThat(writtenPets).containsOnlyKeys("MONSTERS", "FISH"); - }).verifyComplete(); + .flatMap(p -> repository.findById(p.getId())) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(person -> { + Map> writtenPets = person.getPets(); + assertThat(writtenPets).containsOnlyKeys("MONSTERS", "FISH"); + }) + .verifyComplete(); - try (Transaction transaction = driver.session(bookmarkCapture.createSessionConfig()).beginTransaction()) { + try (Transaction transaction = this.driver.session(this.bookmarkCapture.createSessionConfig()) + .beginTransaction()) { long numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Pet) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(3L); numberOfRelations = transaction - .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations").formatted(labelOfTestSubject), - Values.parameters("id", newPerson.getId())) - .single().get("numberOfRelations").asLong(); + .run(("MATCH (t:%s)-[r]->(:Hobby) WHERE id(t) = $id RETURN count(r) as numberOfRelations") + .formatted(this.labelOfTestSubject), Values.parameters("id", newPerson.getId())) + .single() + .get("numberOfRelations") + .asLong(); assertThat(numberOfRelations).isEqualTo(2L); } } - interface PersonWithRelativesRepository extends ReactiveNeo4jRepository {} + interface PersonWithRelativesRepository extends ReactiveNeo4jRepository { + + } @Configuration @EnableTransactionManagement @@ -288,25 +316,30 @@ class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITB static class Config extends Neo4jReactiveTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); } @Override public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTests.java similarity index 58% rename from src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java rename to src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTests.java index acc4554b5..f676d4683 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveTransactionManagerMixedDatabasesTests.java @@ -15,14 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.time.LocalDate; import java.util.Collections; import java.util.Map; @@ -38,6 +30,9 @@ import org.neo4j.driver.reactivestreams.ReactiveResult; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,25 +49,34 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + /** - * The goal of these tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} - * transaction when the user uses the {@link Neo4jClient} in the same or another database. + * The goal of these tests is to ensure a sensible coexistence of declarative + * {@link Transactional @Transactional} transaction when the user uses the + * {@link Neo4jClient} in the same or another database. *

- * While it does not integrate against a real database (multi-database is an enterprise feature), it is still an - * integration test due to the high integration with Spring framework code. + * While it does not integrate against a real database (multi-database is an enterprise + * feature), it is still an integration test due to the high integration with Spring + * framework code. */ @ExtendWith(SpringExtension.class) -class ReactiveTransactionManagerMixedDatabasesTest { +class ReactiveTransactionManagerMixedDatabasesTests { + + public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; protected static final String DATABASE_NAME = "boom"; - public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; private final Driver driver; private final ReactiveNeo4jTransactionManager neo4jTransactionManager; @Autowired - ReactiveTransactionManagerMixedDatabasesTest(Driver driver, ReactiveNeo4jTransactionManager neo4jTransactionManager) { + ReactiveTransactionManagerMixedDatabasesTests(Driver driver, + ReactiveNeo4jTransactionManager neo4jTransactionManager) { this.driver = driver; this.neo4jTransactionManager = neo4jTransactionManager; @@ -94,12 +98,15 @@ class ReactiveTransactionManagerMixedDatabasesTest { @Test void usingSameDatabaseExplicitTx(@Autowired ReactiveNeo4jClient neo4jClient) { - ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(driver, + ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(this.driver, ReactiveDatabaseSelectionProvider.createStaticDatabaseSelectionProvider(DATABASE_NAME)); TransactionalOperator otherTransactionTemplate = TransactionalOperator.create(otherTransactionManger); - Mono numberOfNodes = neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one() - .as(otherTransactionTemplate::transactional); + Mono numberOfNodes = neo4jClient.query(TEST_QUERY) + .in(DATABASE_NAME) + .fetchAs(Long.class) + .one() + .as(otherTransactionTemplate::transactional); StepVerifier.create(numberOfNodes).expectNext(1L).verifyComplete(); } @@ -108,46 +115,52 @@ class ReactiveTransactionManagerMixedDatabasesTest { void usingAnotherDatabaseDeclarative(@Autowired WrapperService wrapperService) { StepVerifier.create(wrapperService.usingAnotherDatabaseDeclarative()) - .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() - .equals("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'")) - .verify(); + .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() + .equals("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'")) + .verify(); } @Test void usingAnotherDatabaseExplicitTx(@Autowired ReactiveNeo4jClient neo4jClient) { - TransactionalOperator transactionTemplate = TransactionalOperator.create(neo4jTransactionManager); + TransactionalOperator transactionTemplate = TransactionalOperator.create(this.neo4jTransactionManager); - Mono numberOfNodes = neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class) - .one().as(transactionTemplate::transactional); + Mono numberOfNodes = neo4jClient.query("MATCH (n) RETURN COUNT(n)") + .in(DATABASE_NAME) + .fetchAs(Long.class) + .one() + .as(transactionTemplate::transactional); StepVerifier.create(numberOfNodes) - .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() - .equals("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'")) - .verify(); + .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() + .equals("There is already an ongoing Spring transaction for the default user of the default database, but you requested the default user of 'boom'")) + .verify(); } @Test void usingAnotherDatabaseDeclarativeFromRepo(@Autowired ReactivePersonRepository repository) { - ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(driver, + ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(this.driver, ReactiveDatabaseSelectionProvider.createStaticDatabaseSelectionProvider(DATABASE_NAME)); TransactionalOperator otherTransactionTemplate = TransactionalOperator.create(otherTransactionManger); - Mono p = repository.save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", - true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null)) - .as(otherTransactionTemplate::transactional); + Mono p = repository + .save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null)) + .as(otherTransactionTemplate::transactional); StepVerifier.create(p) - .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() - .equals("There is already an ongoing Spring transaction for the default user of 'boom', but you requested the default user of the default database")) - .verify(); + .expectErrorMatches(e -> e instanceof IllegalStateException && e.getMessage() + .equals("There is already an ongoing Spring transaction for the default user of 'boom', but you requested the default user of the default database")) + .verify(); } /** - * We need this wrapper service, as reactive {@link Transactional @Transactional} annotated methods are not recognized - * as such (See other also https://github.com/spring-projects/spring-framework/issues/23277). The class must be public - * to make the declarative transactions work. Please don't change its visibility. + * We need this wrapper service, as reactive {@link Transactional @Transactional} + * annotated methods are not recognized as such (See other also + * https://github.com/spring-projects/spring-framework/issues/23277). The class must + * be public to make the declarative transactions work. Please don't change its + * visibility. */ public static class WrapperService { @@ -160,13 +173,14 @@ class ReactiveTransactionManagerMixedDatabasesTest { @Transactional public Mono usingTheSameDatabaseDeclarative() { - return neo4jClient.query(TEST_QUERY).fetchAs(Long.class).one(); + return this.neo4jClient.query(TEST_QUERY).fetchAs(Long.class).one(); } @Transactional public Mono usingAnotherDatabaseDeclarative() { - return neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one(); + return this.neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one(); } + } @Configuration @@ -175,60 +189,66 @@ class ReactiveTransactionManagerMixedDatabasesTest { static class Config extends AbstractReactiveNeo4jConfig { @Bean + @Override public Driver driver() { Record boomRecord = mock(Record.class); - when(boomRecord.size()).thenReturn(1); - when(boomRecord.get(0)).thenReturn(Values.value(1L)); + given(boomRecord.size()).willReturn(1); + given(boomRecord.get(0)).willReturn(Values.value(1L)); Record defaultRecord = mock(Record.class); - when(defaultRecord.size()).thenReturn(1); - when(defaultRecord.get(0)).thenReturn(Values.value(0L)); + given(defaultRecord.size()).willReturn(1); + given(defaultRecord.get(0)).willReturn(Values.value(0L)); ReactiveResult boomResult = mock(ReactiveResult.class); - when(boomResult.records()).thenReturn(Mono.just(boomRecord)); - when(boomResult.consume()).thenReturn(Mono.just(mock(ResultSummary.class))); + given(boomResult.records()).willReturn(Mono.just(boomRecord)); + given(boomResult.consume()).willReturn(Mono.just(mock(ResultSummary.class))); ReactiveResult defaultResult = mock(ReactiveResult.class); - when(defaultResult.records()).thenReturn(Mono.just(defaultRecord)); - when(defaultResult.consume()).thenReturn(Mono.just(mock(ResultSummary.class))); + given(defaultResult.records()).willReturn(Mono.just(defaultRecord)); + given(defaultResult.consume()).willReturn(Mono.just(mock(ResultSummary.class))); ReactiveTransaction boomTransaction = mock(ReactiveTransaction.class); - when(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(Mono.just(boomResult)); - when(boomTransaction.commit()).thenReturn(Mono.empty()); - when(boomTransaction.rollback()).thenReturn(Mono.empty()); + given(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).willReturn(Mono.just(boomResult)); + given(boomTransaction.commit()).willReturn(Mono.empty()); + given(boomTransaction.rollback()).willReturn(Mono.empty()); ReactiveTransaction defaultTransaction = mock(ReactiveTransaction.class); - when(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(Mono.just(defaultResult)); - when(defaultTransaction.commit()).thenReturn(Mono.empty()); - when(defaultTransaction.rollback()).thenReturn(Mono.empty()); + given(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).willReturn(Mono.just(defaultResult)); + given(defaultTransaction.commit()).willReturn(Mono.empty()); + given(defaultTransaction.rollback()).willReturn(Mono.empty()); ReactiveSession boomSession = mock(ReactiveSession.class); - when(boomSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(Mono.just(boomResult)); - when(boomSession.beginTransaction()).thenReturn(Mono.just(boomTransaction)); - when(boomSession.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(boomTransaction)); - when(boomSession.close()).thenReturn(Mono.empty()); + given(boomSession.run(eq(TEST_QUERY), any(Map.class))).willReturn(Mono.just(boomResult)); + given(boomSession.beginTransaction()).willReturn(Mono.just(boomTransaction)); + given(boomSession.beginTransaction(any(TransactionConfig.class))).willReturn(Mono.just(boomTransaction)); + given(boomSession.close()).willReturn(Mono.empty()); ReactiveSession defaultSession = mock(ReactiveSession.class); - when(defaultSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(Mono.just(defaultResult)); - when(defaultSession.beginTransaction()).thenReturn(Mono.just(defaultTransaction)); - when(defaultSession.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(defaultTransaction)); - when(defaultSession.close()).thenReturn(Mono.empty()); + given(defaultSession.run(eq(TEST_QUERY), any(Map.class))).willReturn(Mono.just(defaultResult)); + given(defaultSession.beginTransaction()).willReturn(Mono.just(defaultTransaction)); + given(defaultSession.beginTransaction(any(TransactionConfig.class))) + .willReturn(Mono.just(defaultTransaction)); + given(defaultSession.close()).willReturn(Mono.empty()); Driver driver = mock(Driver.class); - when(driver.session(ReactiveSession.class)).thenReturn(defaultSession); - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).then(invocation -> { + given(driver.session(ReactiveSession.class)).willReturn(defaultSession); + given(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).will(invocation -> { SessionConfig sessionConfig = invocation.getArgument(1); - return sessionConfig.database().filter(n -> n.equals(DATABASE_NAME)).map(n -> boomSession) - .orElse(defaultSession); + return sessionConfig.database() + .filter(n -> n.equals(DATABASE_NAME)) + .map(n -> boomSession) + .orElse(defaultSession); }); return driver; } @Bean - public WrapperService wrapperService(ReactiveNeo4jClient reactiveNeo4jClient) { + WrapperService wrapperService(ReactiveNeo4jClient reactiveNeo4jClient) { return new WrapperService(reactiveNeo4jClient); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java index 1e950fb4f..749f28c1d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java @@ -22,4 +22,5 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; * @author Gerrit Meier */ public interface ReactiveFlightRepository extends ReactiveNeo4jRepository { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java index 79b1d9405..be021153a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java @@ -76,12 +76,6 @@ public interface ReactivePersonRepository extends ReactiveNeo4jRepository findAllByPlace(SomethingThatIsNotKnownAsEntity p); - /** - * Needed to have something that is not mapped in to a map. - */ - class SomethingThatIsNotKnownAsEntity { - } - @Query("MATCH (n:PersonWithAllConstructor) where n.name = $name return n{.name}") Mono findByNameWithCustomQueryAndMapProjection(@Param("name") String name); @@ -110,4 +104,12 @@ public interface ReactivePersonRepository extends ReactiveNeo4jRepository deleteAllByName(String name); Mono deleteAllByNameOrName(String name, String otherName); + + /** + * Needed to have something that is not mapped in to a map. + */ + class SomethingThatIsNotKnownAsEntity { + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveScrollingRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveScrollingRepository.java index 35f2b9c4f..110055bc7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveScrollingRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveScrollingRepository.java @@ -17,15 +17,15 @@ package org.springframework.data.neo4j.integration.reactive.repositories; import java.util.UUID; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.neo4j.integration.shared.common.ScrollingEntity; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - /** * @author Michael J. Simons */ @@ -38,4 +38,5 @@ public interface ReactiveScrollingRepository extends ReactiveNeo4jRepository findFirstByA(String a); Flux findAllByAOrderById(String a); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveThingRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveThingRepository.java index a12bbfd05..7258f7a66 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveThingRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveThingRepository.java @@ -28,4 +28,5 @@ public interface ReactiveThingRepository extends ReactiveCrudRepository(b:Thing2) return n, collect(r), collect(b)") Mono getViaQuery(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/package-info.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/package-info.java index 8c156bc8a..f6ef05770 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/package-info.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * Repositories shared between tests. */ diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractNamedThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractNamedThing.java index 2bb20102b..328047133 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractNamedThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractNamedThing.java @@ -18,15 +18,16 @@ package org.springframework.data.neo4j.integration.shared.common; /** * @author Michael J. Simons */ -abstract class AbstractNamedThing { +public abstract class AbstractNamedThing { private String name; public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractPet.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractPet.java index 240aaffa8..f147d35ac 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractPet.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AbstractPet.java @@ -36,4 +36,5 @@ public abstract class AbstractPet { public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Airport.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Airport.java index d6b803e17..d898b0612 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Airport.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Airport.java @@ -25,8 +25,9 @@ import org.springframework.data.neo4j.core.schema.Node; public class Airport { @Id - String code; // e.g. "LAX" - String name; // e.g. "Los Angeles" + String code; // e.g. "LAX" + + String name; // e.g. "Los Angeles" public Airport(String code, String name) { this.code = code; @@ -34,14 +35,15 @@ public class Airport { } public String getCode() { - return code; + return this.code; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AllArgsCtorNoBuilder.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AllArgsCtorNoBuilder.java index 060b0c669..5925f7597 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AllArgsCtorNoBuilder.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AllArgsCtorNoBuilder.java @@ -47,22 +47,23 @@ public class AllArgsCtorNoBuilder { } public Long getId() { - return id; + return this.id; } public boolean isaBoolean() { - return aBoolean; + return this.aBoolean; } public long getaLong() { - return aLong; + return this.aLong; } public double getaDouble() { - return aDouble; + return this.aDouble; } public String getaString() { - return aString; + return this.aString; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltHobby.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltHobby.java index 73e96ac73..31c2e8fe3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltHobby.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltHobby.java @@ -28,6 +28,7 @@ import org.springframework.data.neo4j.core.schema.Relationship; */ @Node public class AltHobby { + @Id @GeneratedValue private Long id; @@ -41,11 +42,11 @@ public class AltHobby { private List memberOf = new ArrayList<>(); public List getMemberOf() { - return memberOf; + return this.memberOf; } public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -53,7 +54,7 @@ public class AltHobby { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -61,6 +62,7 @@ public class AltHobby { } public List getLikedBy() { - return likedBy; + return this.likedBy; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltLikedByPersonRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltLikedByPersonRelationship.java index 17bebabc6..2f9e06582 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltLikedByPersonRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltLikedByPersonRelationship.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; -import java.util.Objects; - /** * @author Michael J. Simons */ @@ -36,7 +36,7 @@ public class AltLikedByPersonRelationship { private AltPerson altPerson; public Integer getRating() { - return rating; + return this.rating; } public void setRating(Integer rating) { @@ -44,7 +44,7 @@ public class AltLikedByPersonRelationship { } public AltPerson getAltPerson() { - return altPerson; + return this.altPerson; } public void setAltPerson(AltPerson altPerson) { @@ -60,19 +60,18 @@ public class AltLikedByPersonRelationship { return false; } AltLikedByPersonRelationship that = (AltLikedByPersonRelationship) o; - return rating.equals(that.rating) && altPerson.equals(that.altPerson); + return this.rating.equals(that.rating) && this.altPerson.equals(that.altPerson); } @Override public int hashCode() { - return Objects.hash(rating, altPerson); + return Objects.hash(this.rating, this.altPerson); } @Override public String toString() { - return "AltLikedByPersonRelationship{" + - "rating=" + rating + - ", altPerson=" + altPerson.getName() + - '}'; + return "AltLikedByPersonRelationship{" + "rating=" + this.rating + ", altPerson=" + this.altPerson.getName() + + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltPerson.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltPerson.java index 53d0499e5..100eb3bf3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltPerson.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AltPerson.java @@ -15,30 +15,30 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + 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 java.util.Objects; - /** * @@author Michael J. Simons */ @Node public class AltPerson { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - public AltPerson(String name) { this.name = name; } public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -46,7 +46,7 @@ public class AltPerson { } public String getName() { - return name; + return this.name; } @Override @@ -58,11 +58,12 @@ public class AltPerson { return false; } AltPerson altPerson = (AltPerson) o; - return id.equals(altPerson.id) && name.equals(altPerson.name); + return this.id.equals(altPerson.id) && this.name.equals(altPerson.name); } @Override public int hashCode() { - return Objects.hash(id, name); + return Objects.hash(this.id, this.name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AlwaysTheSameIdGenerator.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AlwaysTheSameIdGenerator.java index d00e4909c..05decd5d9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AlwaysTheSameIdGenerator.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AlwaysTheSameIdGenerator.java @@ -28,4 +28,5 @@ public class AlwaysTheSameIdGenerator implements IdGenerator { public String generateId(String primaryLabel, Object entity) { return primaryLabel; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AnotherThingWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AnotherThingWithAssignedId.java index 44a30c33d..1224d79e6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AnotherThingWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AnotherThingWithAssignedId.java @@ -36,11 +36,11 @@ public class AnotherThingWithAssignedId { } public Long getTheId() { - return theId; + return this.theId; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -56,11 +56,12 @@ public class AnotherThingWithAssignedId { return false; } AnotherThingWithAssignedId that = (AnotherThingWithAssignedId) o; - return theId.equals(that.theId) && Objects.equals(name, that.name); + return this.theId.equals(that.theId) && Objects.equals(this.name, that.name); } @Override public int hashCode() { - return Objects.hash(theId, name); + return Objects.hash(this.theId, this.name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditableThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditableThing.java index 9c7a82d55..15be0565c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditableThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditableThing.java @@ -31,4 +31,5 @@ public interface AuditableThing { String getModifiedBy(); String getName(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditingITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditingITBase.java index d5dfb90c3..54b2d24b7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditingITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/AuditingITBase.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; @@ -26,10 +24,13 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; + import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import static org.assertj.core.api.Assertions.assertThat; + /** * Shared information for both imperative and reactive auditing tests. * @@ -37,18 +38,23 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; */ @Neo4jIntegrationTest public abstract class AuditingITBase { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + public static final LocalDateTime DEFAULT_CREATION_AND_MODIFICATION_DATE = LocalDateTime.of(2018, 7, 1, 8, 0); protected static final String EXISTING_THING_NAME = "An old name"; + protected static final String EXISTING_THING_CREATED_BY = "The creator"; + protected static final LocalDateTime EXISTING_THING_CREATED_AT = LocalDateTime.of(2013, 5, 6, 8, 0); - public static final LocalDateTime DEFAULT_CREATION_AND_MODIFICATION_DATE = LocalDateTime.of(2018, 7, 1, 8, 0); + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; private final Driver driver; private final BookmarkCapture bookmarkCapture; protected Long idOfExistingThing; + protected String idOfExistingThingWithGeneratedId = "somethingUnique"; protected AuditingITBase(Driver driver, BookmarkCapture bookmarkCapture) { @@ -58,32 +64,36 @@ public abstract class AuditingITBase { @BeforeEach protected void setupData() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction()) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); - idOfExistingThing = transaction.run( - "CREATE (t:ImmutableAuditableThing {name: $name, createdBy: $createdBy, createdAt: $createdAt}) RETURN id(t) as id", - Values.parameters("name", EXISTING_THING_NAME, "createdBy", EXISTING_THING_CREATED_BY, "createdAt", - EXISTING_THING_CREATED_AT)) - .single().get("id").asLong(); + this.idOfExistingThing = transaction.run( + "CREATE (t:ImmutableAuditableThing {name: $name, createdBy: $createdBy, createdAt: $createdAt}) RETURN id(t) as id", + Values.parameters("name", EXISTING_THING_NAME, "createdBy", EXISTING_THING_CREATED_BY, "createdAt", + EXISTING_THING_CREATED_AT)) + .single() + .get("id") + .asLong(); transaction.run( "CREATE (t:ImmutableAuditableThingWithGeneratedId {name: $name, createdBy: $createdBy, createdAt: $createdAt, id: $id}) RETURN t.id as id", Values.parameters("name", EXISTING_THING_NAME, "createdBy", EXISTING_THING_CREATED_BY, "createdAt", - EXISTING_THING_CREATED_AT, "id", idOfExistingThingWithGeneratedId)); + EXISTING_THING_CREATED_AT, "id", this.idOfExistingThingWithGeneratedId)); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } protected void verifyDatabase(long id, ImmutableAuditableThing expectedValues) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Node node = session - .run("MATCH (t:ImmutableAuditableThing) WHERE id(t) = $id RETURN t", Values.parameters("id", id)).single() - .get("t").asNode(); + .run("MATCH (t:ImmutableAuditableThing) WHERE id(t) = $id RETURN t", Values.parameters("id", id)) + .single() + .get("t") + .asNode(); assertDataMatch(expectedValues, node); } @@ -91,9 +101,13 @@ public abstract class AuditingITBase { protected void verifyDatabase(String id, ImmutableAuditableThingWithGeneratedId expectedValues) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Node node = session.run("MATCH (t:ImmutableAuditableThingWithGeneratedId) WHERE t.id = $id RETURN t", - Values.parameters("id", id)).single().get("t").asNode(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + Node node = session + .run("MATCH (t:ImmutableAuditableThingWithGeneratedId) WHERE t.id = $id RETURN t", + Values.parameters("id", id)) + .single() + .get("t") + .asNode(); assertDataMatch(expectedValues, node); } @@ -103,7 +117,8 @@ public abstract class AuditingITBase { assertThat(node.get("name").asString()).isEqualTo(expectedValues.getName()); if (expectedValues.getCreatedAt() == null) { assertThat(node.get("createdAt").isNull()).isTrue(); - } else { + } + else { assertThat(node.get("createdAt").asLocalDateTime()).isEqualTo(expectedValues.getCreatedAt()); } assertThat(node.get("createdBy").asString()).isEqualTo(expectedValues.getCreatedBy()); @@ -113,14 +128,17 @@ public abstract class AuditingITBase { if (expectedValues.getModifiedAt() == null) { assertThat(modifiedAt.isNull()).isTrue(); - } else { + } + else { assertThat(modifiedAt.asLocalDateTime()).isEqualTo(expectedValues.getModifiedAt()); } if (expectedValues.getModifiedBy() == null) { assertThat(modifiedBy.isNull()).isTrue(); - } else { + } + else { assertThat(modifiedBy.asString()).isEqualTo(expectedValues.getModifiedBy()); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalAssignedId.java index 8afafd4cf..b505813f0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalAssignedId.java @@ -15,12 +15,12 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.UUID; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; -import java.util.UUID; - /** * Bidirectional relationship persisting with assigned id. */ diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalEnd.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalEnd.java index b3cf7cb28..1cd994a24 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalEnd.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalEnd.java @@ -35,8 +35,7 @@ public class BidirectionalEnd { @Relationship(type = "CONNECTED", direction = Relationship.Direction.INCOMING) private BidirectionalStart start; - @Relationship(type = "ANOTHER_CONNECTION", - direction = Relationship.Direction.INCOMING) + @Relationship(type = "ANOTHER_CONNECTION", direction = Relationship.Direction.INCOMING) private BidirectionalStart anotherStart; public BidirectionalEnd(String name) { @@ -44,14 +43,15 @@ public class BidirectionalEnd { } public BidirectionalStart getStart() { - return start; - } - - public BidirectionalStart getAnotherStart() { - return anotherStart; + return this.start; } public void setStart(BidirectionalStart start) { this.start = start; } + + public BidirectionalStart getAnotherStart() { + return this.anotherStart; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java index 0cb55cdfc..2bee2fbc0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java @@ -15,13 +15,13 @@ */ package org.springframework.data.neo4j.integration.shared.common; +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.Relationship; -import java.util.UUID; - /** * Bidirectional relationship persisting with externally generated id. */ @@ -36,10 +36,11 @@ public class BidirectionalExternallyGeneratedId { public BidirectionalExternallyGeneratedId otter; public UUID getUuid() { - return uuid; + return this.uuid; } public BidirectionalExternallyGeneratedId getOtter() { - return otter; + return this.otter; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalSameEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalSameEntity.java index d3df5dd3d..0cfd6bff5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalSameEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalSameEntity.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.List; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -23,8 +25,6 @@ import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; -import java.util.List; - /** * @author Gerrit Meier */ @@ -51,6 +51,9 @@ public class BidirectionalSameEntity { @RelationshipProperties public static class BidirectionalSameRelationship { + @TargetNode + BidirectionalSameEntity entity; + @RelationshipId private Long id; @@ -58,7 +61,6 @@ public class BidirectionalSameEntity { this.entity = entity; } - @TargetNode - BidirectionalSameEntity entity; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalStart.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalStart.java index 402df360c..f13857072 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalStart.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalStart.java @@ -43,10 +43,11 @@ public class BidirectionalStart { } public String getName() { - return name; + return this.name; } public Set getEnds() { - return ends; + return this.ends; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Book.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Book.java index 48f2b297a..a2203c9ae 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Book.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Book.java @@ -43,10 +43,13 @@ public class Book { @CreatedDate private LocalDateTime createdAt; + @CreatedBy private String createdBy; + @LastModifiedDate private LocalDateTime modifiedAt; + @LastModifiedBy private String modifiedBy; @@ -58,15 +61,19 @@ public class Book { } public UUID getId() { - return id; + return this.id; } public String getTitle() { - return title; + return this.title; + } + + public void setTitle(String title) { + this.title = title; } public String getContent() { - return content; + return this.content; } public void setContent(String content) { @@ -74,7 +81,7 @@ public class Book { } public LocalDateTime getCreatedAt() { - return createdAt; + return this.createdAt; } public void setCreatedAt(LocalDateTime createdAt) { @@ -82,7 +89,7 @@ public class Book { } public String getCreatedBy() { - return createdBy; + return this.createdBy; } public void setCreatedBy(String createdBy) { @@ -90,7 +97,7 @@ public class Book { } public LocalDateTime getModifiedAt() { - return modifiedAt; + return this.modifiedAt; } public void setModifiedAt(LocalDateTime modifiedAt) { @@ -98,22 +105,19 @@ public class Book { } public String getModifiedBy() { - return modifiedBy; + return this.modifiedBy; } public void setModifiedBy(String modifiedBy) { this.modifiedBy = modifiedBy; } - public void setTitle(String title) { - this.title = title; - } - public Editor getPreviousEditor() { - return previousEditor; + return this.previousEditor; } public void setPreviousEditor(Editor previousEditor) { this.previousEditor = previousEditor; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/CallbacksITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/CallbacksITBase.java index 6880f35a3..438b01a5d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/CallbacksITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/CallbacksITBase.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -29,10 +27,13 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; + import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import static org.assertj.core.api.Assertions.assertThat; + /** * Shared information for both imperative and reactive callbacks tests. * @@ -42,6 +43,7 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; public abstract class CallbacksITBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private final BookmarkCapture bookmarkCapture; private final Driver driver; @@ -54,26 +56,28 @@ public abstract class CallbacksITBase { @BeforeEach protected void setupData() { - try (Session session = driver.session()) { + try (Session session = this.driver.session()) { try (Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("UNWIND ['E1', 'E2'] AS id WITH id CREATE (t:Thing {theId: id, name: 'Egal'})"); transaction.commit(); } - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } protected void verifyDatabase(Iterable expectedValues) { - List ids = StreamSupport.stream(expectedValues.spliterator(), false).map(ThingWithAssignedId::getTheId) - .collect(Collectors.toList()); - List names = StreamSupport.stream(expectedValues.spliterator(), false).map(ThingWithAssignedId::getName) - .collect(Collectors.toList()); - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + List ids = StreamSupport.stream(expectedValues.spliterator(), false) + .map(ThingWithAssignedId::getTheId) + .collect(Collectors.toList()); + List names = StreamSupport.stream(expectedValues.spliterator(), false) + .map(ThingWithAssignedId::getName) + .collect(Collectors.toList()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Record record = session - .run("MATCH (n:Thing) WHERE n.theId in $ids RETURN COLLECT(n) as things", Values.parameters("ids", ids)) - .single(); + .run("MATCH (n:Thing) WHERE n.theId in $ids RETURN COLLECT(n) as things", Values.parameters("ids", ids)) + .single(); List nodes = record.get("things").asList(Value::asNode); assertThat(nodes).extracting(n -> n.get("theId").asString()).containsAll(ids); @@ -81,7 +85,8 @@ public abstract class CallbacksITBase { assertThat(nodes).allMatch(n -> n.get("randomValue").isNull()); assertThat(nodes).allMatch(n -> n.get("anotherRandomValue").isNull()); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Club.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Club.java index e55abf6b8..c38accadc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Club.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Club.java @@ -32,14 +32,15 @@ public class Club { private String name; public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ClubRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ClubRelationship.java index 27c15e327..b7b9bf648 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ClubRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ClubRelationship.java @@ -38,18 +38,19 @@ public class ClubRelationship { } public String getPlace() { - return place; + return this.place; } public void setPlace(String place) { this.place = place; } + public Club getClub() { + return this.club; + } + public void setClub(Club club) { this.club = club; } - public Club getClub() { - return club; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/CounterMetric.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/CounterMetric.java index 837051f48..ef36723ce 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/CounterMetric.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/CounterMetric.java @@ -26,4 +26,5 @@ public class CounterMetric extends Metric { public CounterMetric(String name) { super(name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DeepRelationships.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DeepRelationships.java index 26eb6d034..40a867638 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DeepRelationships.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DeepRelationships.java @@ -32,10 +32,13 @@ public class DeepRelationships { */ @Node public static class LoopingType1 { + public LoopingType2 nextType; + @Id @GeneratedValue private Long id; + } /** @@ -43,10 +46,13 @@ public class DeepRelationships { */ @Node public static class LoopingType2 { + public LoopingType3 nextType; + @Id @GeneratedValue private Long id; + } /** @@ -54,9 +60,13 @@ public class DeepRelationships { */ @Node public static class LoopingType3 { + public LoopingType1 nextType; + @Id @GeneratedValue private Long id; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DepartmentEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DepartmentEntity.java index 9e3eed772..2f26afb2a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DepartmentEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DepartmentEntity.java @@ -26,6 +26,7 @@ public class DepartmentEntity { @Id private final String id; + private final String name; public DepartmentEntity(String id, String name) { @@ -40,4 +41,5 @@ public class DepartmentEntity { public String getName() { return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DoritoEatingPerson.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DoritoEatingPerson.java index 62b430a2f..f590300fd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DoritoEatingPerson.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DoritoEatingPerson.java @@ -15,14 +15,15 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + 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; -import java.util.HashSet; -import java.util.Set; - /** * @author Michael J. Simons */ @@ -48,7 +49,8 @@ public class DoritoEatingPerson { this.name = name; } - public DoritoEatingPerson(long id, String name, boolean eatsDoritos, boolean friendsAlsoEatDoritos, Set friends) { + public DoritoEatingPerson(long id, String name, boolean eatsDoritos, boolean friendsAlsoEatDoritos, + Set friends) { this.id = id; this.name = name; this.eatsDoritos = eatsDoritos; @@ -63,42 +65,47 @@ public class DoritoEatingPerson { return this.id; } - public String getName() { - return this.name; - } - - public boolean isEatsDoritos() { - return this.eatsDoritos; - } - - public boolean isFriendsAlsoEatDoritos() { - return this.friendsAlsoEatDoritos; - } - - public Set getFriends() { - return this.friends; - } - public void setId(long id) { this.id = id; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public boolean isEatsDoritos() { + return this.eatsDoritos; + } + public void setEatsDoritos(boolean eatsDoritos) { this.eatsDoritos = eatsDoritos; } + public boolean isFriendsAlsoEatDoritos() { + return this.friendsAlsoEatDoritos; + } + public void setFriendsAlsoEatDoritos(boolean friendsAlsoEatDoritos) { this.friendsAlsoEatDoritos = friendsAlsoEatDoritos; } + public Set getFriends() { + return this.friends; + } + public void setFriends(Set friends) { this.friends = friends; } + protected boolean canEqual(final Object other) { + return other instanceof DoritoEatingPerson; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -115,7 +122,7 @@ public class DoritoEatingPerson { } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } if (this.isEatsDoritos() != other.isEatsDoritos()) { @@ -126,32 +133,28 @@ public class DoritoEatingPerson { } final Object this$friends = this.getFriends(); final Object other$friends = other.getFriends(); - if (this$friends == null ? other$friends != null : !this$friends.equals(other$friends)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof DoritoEatingPerson; + return Objects.equals(this$friends, other$friends); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final long $id = this.getId(); result = result * PRIME + (int) ($id >>> 32 ^ $id); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); result = result * PRIME + (this.isEatsDoritos() ? 79 : 97); result = result * PRIME + (this.isFriendsAlsoEatDoritos() ? 79 : 97); final Object $friends = this.getFriends(); - result = result * PRIME + ($friends == null ? 43 : $friends.hashCode()); + result = result * PRIME + (($friends != null) ? $friends.hashCode() : 43); return result; } + @Override public String toString() { - return "DoritoEatingPerson(id=" + this.getId() + ", name=" + this.getName() + ", eatsDoritos=" + this.isEatsDoritos() + ", friendsAlsoEatDoritos=" + this.isFriendsAlsoEatDoritos() + ")"; + return "DoritoEatingPerson(id=" + this.getId() + ", name=" + this.getName() + ", eatsDoritos=" + + this.isEatsDoritos() + ", friendsAlsoEatDoritos=" + this.isFriendsAlsoEatDoritos() + ")"; } /** @@ -162,6 +165,7 @@ public class DoritoEatingPerson { boolean getEatsDoritos(); boolean getFriendsAlsoEatDoritos(); + } /** @@ -170,5 +174,7 @@ public class DoritoEatingPerson { public interface PropertiesProjection2 { boolean getEatsDoritos(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjection.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjection.java index 801b95eef..08d2a17a4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjection.java @@ -15,13 +15,17 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + /** * @author Michael J. Simon */ public final class DtoPersonProjection { private final String name; + private final String sameValue; + private final String firstName; public DtoPersonProjection(String name, String sameValue, String firstName) { @@ -42,6 +46,7 @@ public final class DtoPersonProjection { return this.firstName; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -52,35 +57,36 @@ public final class DtoPersonProjection { final DtoPersonProjection other = (DtoPersonProjection) o; final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$sameValue = this.getSameValue(); final Object other$sameValue = other.getSameValue(); - if (this$sameValue == null ? other$sameValue != null : !this$sameValue.equals(other$sameValue)) { + if (!Objects.equals(this$sameValue, other$sameValue)) { return false; } final Object this$firstName = this.getFirstName(); final Object other$firstName = other.getFirstName(); - if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) { - return false; - } - return true; + return Objects.equals(this$firstName, other$firstName); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $sameValue = this.getSameValue(); - result = result * PRIME + ($sameValue == null ? 43 : $sameValue.hashCode()); + result = result * PRIME + (($sameValue != null) ? $sameValue.hashCode() : 43); final Object $firstName = this.getFirstName(); - result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode()); + result = result * PRIME + (($firstName != null) ? $firstName.hashCode() : 43); return result; } + @Override public String toString() { - return "DtoPersonProjection(name=" + this.getName() + ", sameValue=" + this.getSameValue() + ", firstName=" + this.getFirstName() + ")"; + return "DtoPersonProjection(name=" + this.getName() + ", sameValue=" + this.getSameValue() + ", firstName=" + + this.getFirstName() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjectionContainingAdditionalFields.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjectionContainingAdditionalFields.java index 3e3d85875..efac9364a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjectionContainingAdditionalFields.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DtoPersonProjectionContainingAdditionalFields.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.integration.shared.common; import java.util.List; +import java.util.Objects; /** * @author Michael J. Simons @@ -23,7 +24,9 @@ import java.util.List; public final class DtoPersonProjectionContainingAdditionalFields { private final String name; + private final String sameValue; + private final String firstName; private final List otherPeople; @@ -32,7 +35,8 @@ public final class DtoPersonProjectionContainingAdditionalFields { private final List someDoubles; - public DtoPersonProjectionContainingAdditionalFields(String name, String sameValue, String firstName, List otherPeople, Long someLongValue, List someDoubles) { + public DtoPersonProjectionContainingAdditionalFields(String name, String sameValue, String firstName, + List otherPeople, Long someLongValue, List someDoubles) { this.name = name; this.sameValue = sameValue; this.firstName = firstName; @@ -65,6 +69,7 @@ public final class DtoPersonProjectionContainingAdditionalFields { return this.someDoubles; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -75,56 +80,58 @@ public final class DtoPersonProjectionContainingAdditionalFields { final DtoPersonProjectionContainingAdditionalFields other = (DtoPersonProjectionContainingAdditionalFields) o; final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$sameValue = this.getSameValue(); final Object other$sameValue = other.getSameValue(); - if (this$sameValue == null ? other$sameValue != null : !this$sameValue.equals(other$sameValue)) { + if (!Objects.equals(this$sameValue, other$sameValue)) { return false; } final Object this$firstName = this.getFirstName(); final Object other$firstName = other.getFirstName(); - if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) { + if (!Objects.equals(this$firstName, other$firstName)) { return false; } final Object this$otherPeople = this.getOtherPeople(); final Object other$otherPeople = other.getOtherPeople(); - if (this$otherPeople == null ? other$otherPeople != null : !this$otherPeople.equals(other$otherPeople)) { + if (!Objects.equals(this$otherPeople, other$otherPeople)) { return false; } final Object this$someLongValue = this.getSomeLongValue(); final Object other$someLongValue = other.getSomeLongValue(); - if (this$someLongValue == null ? other$someLongValue != null : !this$someLongValue.equals(other$someLongValue)) { + if (!Objects.equals(this$someLongValue, other$someLongValue)) { return false; } final Object this$someDoubles = this.getSomeDoubles(); final Object other$someDoubles = other.getSomeDoubles(); - if (this$someDoubles == null ? other$someDoubles != null : !this$someDoubles.equals(other$someDoubles)) { - return false; - } - return true; + return Objects.equals(this$someDoubles, other$someDoubles); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $sameValue = this.getSameValue(); - result = result * PRIME + ($sameValue == null ? 43 : $sameValue.hashCode()); + result = result * PRIME + (($sameValue != null) ? $sameValue.hashCode() : 43); final Object $firstName = this.getFirstName(); - result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode()); + result = result * PRIME + (($firstName != null) ? $firstName.hashCode() : 43); final Object $otherPeople = this.getOtherPeople(); - result = result * PRIME + ($otherPeople == null ? 43 : $otherPeople.hashCode()); + result = result * PRIME + (($otherPeople != null) ? $otherPeople.hashCode() : 43); final Object $someLongValue = this.getSomeLongValue(); - result = result * PRIME + ($someLongValue == null ? 43 : $someLongValue.hashCode()); + result = result * PRIME + (($someLongValue != null) ? $someLongValue.hashCode() : 43); final Object $someDoubles = this.getSomeDoubles(); - result = result * PRIME + ($someDoubles == null ? 43 : $someDoubles.hashCode()); + result = result * PRIME + (($someDoubles != null) ? $someDoubles.hashCode() : 43); return result; } + @Override public String toString() { - return "DtoPersonProjectionContainingAdditionalFields(name=" + this.getName() + ", sameValue=" + this.getSameValue() + ", firstName=" + this.getFirstName() + ", otherPeople=" + this.getOtherPeople() + ", someLongValue=" + this.getSomeLongValue() + ", someDoubles=" + this.getSomeDoubles() + ")"; + return "DtoPersonProjectionContainingAdditionalFields(name=" + this.getName() + ", sameValue=" + + this.getSameValue() + ", firstName=" + this.getFirstName() + ", otherPeople=" + this.getOtherPeople() + + ", someLongValue=" + this.getSomeLongValue() + ", someDoubles=" + this.getSomeDoubles() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DynamicRelationshipsITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DynamicRelationshipsITBase.java index fc4c3af73..e4589b2dc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/DynamicRelationshipsITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/DynamicRelationshipsITBase.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.BeforeEach; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; @@ -29,9 +30,8 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; /** * Make sure that dynamic relationships can be loaded and stored. * - * @author Michael J. Simons * @param Type of the person with relatives - * @soundtrack Helge Schneider - Live At The Grugahalle + * @author Michael J. Simons */ @Neo4jIntegrationTest public abstract class DynamicRelationshipsITBase { @@ -39,12 +39,13 @@ public abstract class DynamicRelationshipsITBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; protected final Driver driver; + protected final BookmarkCapture bookmarkCapture; - protected long idOfExistingPerson; - protected final String labelOfTestSubject; + protected long idOfExistingPerson; + protected DynamicRelationshipsITBase(Driver driver, BookmarkCapture bookmarkCapture) { this.driver = driver; this.bookmarkCapture = bookmarkCapture; @@ -56,7 +57,7 @@ public abstract class DynamicRelationshipsITBase { @BeforeEach protected void setupData() { - try (Session session = driver.session(); Transaction transaction = session.beginTransaction()) { + try (Session session = this.driver.session(); Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); var cypher = """ CREATE (t:%s {name: 'A'}) WITH t\s @@ -67,11 +68,11 @@ public abstract class DynamicRelationshipsITBase { UNWIND ['Tom', 'Garfield'] AS cat CREATE (t) - [:CATS] -> (w:Pet {name: cat})\s WITH DISTINCT t UNWIND ['Benji', 'Lassie'] AS dog\s CREATE (t) - [:DOGS] -> (w:Pet {name: dog}) RETURN DISTINCT id(t) as id - """.formatted(labelOfTestSubject); - idOfExistingPerson = transaction.run(cypher) - .single().get("id").asLong(); + """.formatted(this.labelOfTestSubject); + this.idOfExistingPerson = transaction.run(cypher).single().get("id").asLong(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Editor.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Editor.java index 09efc4ff9..a3fec3f11 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Editor.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Editor.java @@ -27,12 +27,13 @@ import org.springframework.data.neo4j.core.schema.Relationship; */ @Node public class Editor { + + String name; + @Id @GeneratedValue private UUID id; - String name; - @Relationship("HAS_PREDECESSOR") private Editor predecessor; @@ -42,10 +43,11 @@ public class Editor { } public String getName() { - return name; + return this.name; } public Editor getPredecessor() { - return predecessor; + return this.predecessor; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntitiesWithDynamicLabels.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntitiesWithDynamicLabels.java index e422dfe6b..e22c7d99b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntitiesWithDynamicLabels.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntitiesWithDynamicLabels.java @@ -25,10 +25,12 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons - * @soundtrack Samy Deluxe - Samy Deluxe */ public final class EntitiesWithDynamicLabels { + private EntitiesWithDynamicLabels() { + } + /** * Used for testing whether related nodes store their dynamic labels. */ @@ -42,8 +44,9 @@ public final class EntitiesWithDynamicLabels { public SimpleDynamicLabels relatedTo; public SimpleDynamicLabels getRelatedTo() { - return relatedTo; + return this.relatedTo; } + } /** @@ -60,8 +63,9 @@ public final class EntitiesWithDynamicLabels { public Set moreLabels; public Long getId() { - return id; + return this.id; } + } /** @@ -69,6 +73,7 @@ public final class EntitiesWithDynamicLabels { */ @Node public static class InheritedSimpleDynamicLabels extends SimpleDynamicLabels { + } /** @@ -88,8 +93,9 @@ public final class EntitiesWithDynamicLabels { public Set moreLabels; public Long getId() { - return id; + return this.id; } + } /** @@ -105,8 +111,9 @@ public final class EntitiesWithDynamicLabels { public Set moreLabels; public String getId() { - return id; + return this.id; } + } /** @@ -125,8 +132,9 @@ public final class EntitiesWithDynamicLabels { public Set moreLabels; public String getId() { - return id; + return this.id; } + } /** @@ -135,17 +143,18 @@ public final class EntitiesWithDynamicLabels { @Node public static class SimpleDynamicLabelsCtor { + @DynamicLabels + public final Set moreLabels; + @Id @GeneratedValue private final Long id; - @DynamicLabels - public final Set moreLabels; - public SimpleDynamicLabelsCtor(Long id, Set moreLabels) { this.id = id; this.moreLabels = moreLabels; } + } /** @@ -154,34 +163,37 @@ public final class EntitiesWithDynamicLabels { @Node("Baz") public static class DynamicLabelsWithNodeLabel { + @DynamicLabels + public Set moreLabels; + @Id @GeneratedValue private Long id; - @DynamicLabels - public Set moreLabels; } /** * Dynamic labels together with multiple labels. */ - @Node({"Foo", "Bar"}) + @Node({ "Foo", "Bar" }) public static class DynamicLabelsWithMultipleNodeLabels { - @Id - @GeneratedValue - private Long id; - @DynamicLabels public Set moreLabels; - } - - @Node - static abstract class DynamicLabelsBaseClass { @Id @GeneratedValue private Long id; + + } + + @Node + abstract static class DynamicLabelsBaseClass { + + @Id + @GeneratedValue + private Long id; + } /** @@ -192,6 +204,7 @@ public final class EntitiesWithDynamicLabels { @DynamicLabels public Set moreLabels; + } /** @@ -205,31 +218,36 @@ public final class EntitiesWithDynamicLabels { @DynamicLabels public Set myLabels; + } /** * Base entity for the multi-level abstraction */ @Node - public static abstract class BaseEntityWithoutDynamicLabels { + public abstract static class BaseEntityWithoutDynamicLabels { + @Id public String id; + } /** * adds the labels */ @Node - public static abstract class AbstractBaseEntityWithDynamicLabels extends BaseEntityWithoutDynamicLabels { + public abstract static class AbstractBaseEntityWithDynamicLabels extends BaseEntityWithoutDynamicLabels { + @DynamicLabels public Set labels; + } /** * This might be the wrong most concrete class to be found */ @Node - public static abstract class AbstractEntityWithDynamicLabels extends AbstractBaseEntityWithDynamicLabels { + public abstract static class AbstractEntityWithDynamicLabels extends AbstractBaseEntityWithDynamicLabels { } @@ -238,9 +256,9 @@ public final class EntitiesWithDynamicLabels { */ @Node public static class EntityWithMultilevelInheritanceAndDynamicLabels extends AbstractEntityWithDynamicLabels { + public String name; + } - private EntitiesWithDynamicLabels() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithConvertedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithConvertedId.java index 1c21c6b77..f1f414a28 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithConvertedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithConvertedId.java @@ -28,7 +28,7 @@ public class EntityWithConvertedId { private IdentifyingEnum identifyingEnum; public IdentifyingEnum getIdentifyingEnum() { - return identifyingEnum; + return this.identifyingEnum; } public void setIdentifyingEnum(IdentifyingEnum identifyingEnum) { @@ -39,6 +39,9 @@ public class EntityWithConvertedId { * Could also be another type that gets converted inside the framework */ public enum IdentifyingEnum { + A, B + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.java index a4a967a62..b268e120e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.java @@ -26,12 +26,14 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; /** - * Provided via Github as reproducer for entities with dynamic labels and ids that are subject to conversion. Needed for GH-2296. + * Provided via Github as reproducer for entities with dynamic labels and ids that are + * subject to conversion. Needed for GH-2296. * * @author Michael J. Simons */ @Node public class EntityWithDynamicLabelsAndIdThatNeedsToBeConverted { + @Id @GeneratedValue private UUID id; @@ -45,24 +47,25 @@ public class EntityWithDynamicLabelsAndIdThatNeedsToBeConverted { setValue(value); } + public String getValue() { + return this.value; + } + public void setValue(String value) { this.value = value; - if (Objects.isNull(extraLabels)) { - extraLabels = new HashSet<>(); + if (Objects.isNull(this.extraLabels)) { + this.extraLabels = new HashSet<>(); } - extraLabels.add(value); - } - - public String getValue() { - return value; + this.extraLabels.add(value); } public Set getExtraLabels() { - return extraLabels; + return this.extraLabels; } public UUID getId() { - return id; + return this.id; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithPrimitiveConstructorArguments.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithPrimitiveConstructorArguments.java index 74e633848..d26fb6e3f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithPrimitiveConstructorArguments.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithPrimitiveConstructorArguments.java @@ -24,15 +24,18 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class EntityWithPrimitiveConstructorArguments { + + public final boolean someBooleanValue; + + public final int someIntValue; + @Id @GeneratedValue public Long id; - public final boolean someBooleanValue; - public final int someIntValue; - public EntityWithPrimitiveConstructorArguments(boolean someBooleanValue, int someIntValue) { this.someBooleanValue = someBooleanValue; this.someIntValue = someIntValue; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithRelationshipPropertiesPath.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithRelationshipPropertiesPath.java index 3b551488e..90be7a15d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithRelationshipPropertiesPath.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/EntityWithRelationshipPropertiesPath.java @@ -37,7 +37,7 @@ public class EntityWithRelationshipPropertiesPath { private RelationshipPropertyA relationshipA; public RelationshipPropertyA getRelationshipA() { - return relationshipA; + return this.relationshipA; } /** @@ -53,8 +53,9 @@ public class EntityWithRelationshipPropertiesPath { private EntityA entityA; public EntityA getEntityA() { - return entityA; + return this.entityA; } + } /** @@ -70,8 +71,9 @@ public class EntityWithRelationshipPropertiesPath { private EntityB entityB; public EntityB getEntityB() { - return entityB; + return this.entityB; } + } /** @@ -79,6 +81,7 @@ public class EntityWithRelationshipPropertiesPath { */ @Node public static class EntityA { + @Id @GeneratedValue private Long id; @@ -87,8 +90,9 @@ public class EntityWithRelationshipPropertiesPath { private RelationshipPropertyB relationshipB; public RelationshipPropertyB getRelationshipB() { - return relationshipB; + return this.relationshipB; } + } /** @@ -96,11 +100,11 @@ public class EntityWithRelationshipPropertiesPath { */ @Node public static class EntityB { + @Id @GeneratedValue private Long id; } - } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ExtendedParentNode.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ExtendedParentNode.java index b7514ca9f..80955da91 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ExtendedParentNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ExtendedParentNode.java @@ -22,7 +22,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Zurück zum Glück */ @Node public class ExtendedParentNode extends ParentNode { @@ -33,7 +32,7 @@ public class ExtendedParentNode extends ParentNode { private List people; public String getSomeOtherAttribute() { - return someOtherAttribute; + return this.someOtherAttribute; } public void setSomeOtherAttribute(String someOtherAttribute) { @@ -41,10 +40,11 @@ public class ExtendedParentNode extends ParentNode { } public List getPeople() { - return people; + return this.people; } public void setPeople(List people) { this.people = people; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Flight.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Flight.java index 3e04ac842..075331f3c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Flight.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Flight.java @@ -26,17 +26,18 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class Flight { - @Id - @GeneratedValue - private Long id; - private final String name; @Relationship(type = "DEPARTS") private final Airport departure; + @Relationship(type = "ARRIVES") private final Airport arrival; + @Id + @GeneratedValue + private Long id; + @Relationship("NEXT_FLIGHT") private Flight nextFlight; @@ -47,15 +48,15 @@ public class Flight { } public String getName() { - return name; + return this.name; } public Airport getDeparture() { - return departure; + return this.departure; } public Airport getArrival() { - return arrival; + return this.arrival; } public Flight getNextFlight() { @@ -65,4 +66,5 @@ public class Flight { public void setNextFlight(Flight nextFlight) { this.nextFlight = nextFlight; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Friend.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Friend.java index 9900d5ae5..be32aef29 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Friend.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Friend.java @@ -15,25 +15,25 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.List; + 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.Relationship; -import java.util.List; - /** * @author Gerrit Meier */ @Node public class Friend { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - @Relationship("KNOWS") private List friends; @@ -42,10 +42,11 @@ public class Friend { } public String getName() { - return name; + return this.name; } public List getFriends() { - return friends; + return this.friends; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/FriendshipRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/FriendshipRelationship.java index 7528ababa..5f9a99536 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/FriendshipRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/FriendshipRelationship.java @@ -25,11 +25,11 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class FriendshipRelationship { + private final Integer since; + @RelationshipId private Long id; - private final Integer since; - @TargetNode private Friend friend; @@ -38,14 +38,15 @@ public class FriendshipRelationship { } public Integer getSince() { - return since; + return this.since; } public Friend getFriend() { - return friend; + return this.friend; } public void setFriend(Friend friend) { this.friend = friend; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/GH2621Domain.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/GH2621Domain.java index 31296ca71..cba764ed0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/GH2621Domain.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/GH2621Domain.java @@ -26,28 +26,33 @@ import org.springframework.data.neo4j.core.schema.Node; */ public final class GH2621Domain { + private GH2621Domain() { + } + /** * A node. */ @Node("GH2621Foo") public static class Foo { + + private final Bar bar; + @Id @GeneratedValue private UUID id; - private final Bar bar; - public Foo(Bar bar) { this.bar = bar; } public UUID getId() { - return id; + return this.id; } public Bar getBar() { - return bar; + return this.bar; } + } /** @@ -55,23 +60,25 @@ public final class GH2621Domain { */ @Node("GH2621Bar") public static class Bar { + + private final String value1; + @Id @GeneratedValue private UUID id; - private final String value1; - public Bar(String value1) { this.value1 = value1; } public UUID getId() { - return id; + return this.id; } public String getValue1() { - return value1; + return this.value1; } + } /** @@ -79,6 +86,7 @@ public final class GH2621Domain { */ @Node("GH2621BarBar") public static class BarBar extends Bar { + private final String value2; public BarBar(String value1, String value2) { @@ -87,14 +95,16 @@ public final class GH2621Domain { } public String getValue2() { - return value2; + return this.value2; } + } /** * Projects {@link Foo} */ public static class FooProjection { + private final BarProjection bar; public FooProjection(BarProjection bar) { @@ -102,14 +112,16 @@ public final class GH2621Domain { } public BarProjection getBar() { - return bar; + return this.bar; } + } /** * Projects {@link Bar} and {@link BarBar} */ public static class BarProjection { + private final String value1; public BarProjection(String value1) { @@ -117,14 +129,16 @@ public final class GH2621Domain { } public String getValue1() { - return value1; + return this.value1; } + } /** * Projects {@link Bar} and {@link BarBar} */ public static class BarBarProjection extends BarProjection { + private final String value2; public BarBarProjection(String value1, String value2) { @@ -133,11 +147,9 @@ public final class GH2621Domain { } public String getValue2() { - return value2; + return this.value2; } + } - - private GH2621Domain() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/GaugeMetric.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/GaugeMetric.java index 1e11c6165..c2d1c4d54 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/GaugeMetric.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/GaugeMetric.java @@ -17,7 +17,6 @@ package org.springframework.data.neo4j.integration.shared.common; import org.springframework.data.neo4j.core.schema.Node; - /** * @author Michael J. Simons */ @@ -27,4 +26,5 @@ public class GaugeMetric extends Metric { public GaugeMetric(String name) { super(name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/HistogramMetric.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/HistogramMetric.java index 1de319641..cf3f45e6e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/HistogramMetric.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/HistogramMetric.java @@ -26,4 +26,5 @@ public class HistogramMetric extends Metric { public HistogramMetric(String name) { super(name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Hobby.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Hobby.java index 0117e390e..ad9312738 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Hobby.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Hobby.java @@ -26,6 +26,7 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class Hobby { + @Id @GeneratedValue private Long id; @@ -33,7 +34,7 @@ public class Hobby { private String name; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -41,18 +42,13 @@ public class Hobby { } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } - @Override - public String toString() { - return "Hobby{" + "id=" + id + ", name='" + name + '\'' + '}'; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -62,11 +58,17 @@ public class Hobby { return false; } Hobby hobby = (Hobby) o; - return id.equals(hobby.id) && name.equals(hobby.name); + return this.id.equals(hobby.id) && this.name.equals(hobby.name); } @Override public int hashCode() { - return Objects.hash(id, name); + return Objects.hash(this.id, this.name); } + + @Override + public String toString() { + return "Hobby{" + "id=" + this.id + ", name='" + this.name + '\'' + '}'; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/HobbyRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/HobbyRelationship.java index 9423ba038..d1a6da957 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/HobbyRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/HobbyRelationship.java @@ -38,14 +38,15 @@ public class HobbyRelationship { } public String getPerformance() { - return performance; + return this.performance; } public Hobby getHobby() { - return hobby; + return this.hobby; } public void setHobby(Hobby hobby) { this.hobby = hobby; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/IdGeneratorsITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/IdGeneratorsITBase.java index 95d47ae11..d0619d663 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/IdGeneratorsITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/IdGeneratorsITBase.java @@ -15,30 +15,31 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.BeforeEach; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; + import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @Neo4jIntegrationTest public abstract class IdGeneratorsITBase { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - protected static final String EXISTING_THING_NAME = "An old name"; protected static final String ID_OF_EXISTING_THING = "not-generated."; + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private final Driver driver; private final BookmarkCapture bookmarkCapture; @@ -51,24 +52,27 @@ public abstract class IdGeneratorsITBase { @BeforeEach protected void setupData() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); - Transaction transaction = session.beginTransaction()) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n"); transaction.run("CREATE (t:ThingWithGeneratedId {name: $name, theId: $theId}) RETURN id(t) as id", Values.parameters("name", EXISTING_THING_NAME, "theId", ID_OF_EXISTING_THING)); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } protected void verifyDatabase(String id, String name) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Node node = session.run("MATCH (t) WHERE t.theId = $theId RETURN t", Values.parameters("theId", id)).single() - .get("t").asNode(); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + Node node = session.run("MATCH (t) WHERE t.theId = $theId RETURN t", Values.parameters("theId", id)) + .single() + .get("t") + .asNode(); assertThat(node.get("name").asString()).isEqualTo(name); assertThat(node.get("theId").asString()).isEqualTo(id); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThing.java index 2d220caf7..222eb6904 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThing.java @@ -15,6 +15,9 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.time.LocalDateTime; +import java.util.Objects; + import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; @@ -24,8 +27,6 @@ import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Persistent; import org.springframework.data.neo4j.core.schema.GeneratedValue; -import java.time.LocalDateTime; - /** * @author Michael J. Simons */ @@ -36,12 +37,16 @@ public final class ImmutableAuditableThing implements AuditableThing { @Id @GeneratedValue private final Long id; + @CreatedDate private final LocalDateTime createdAt; + @CreatedBy private final String createdBy; + @LastModifiedDate private final LocalDateTime modifiedAt; + @LastModifiedBy private final String modifiedBy; @@ -52,7 +57,8 @@ public final class ImmutableAuditableThing implements AuditableThing { } @PersistenceCreator - public ImmutableAuditableThing(Long id, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy, String name) { + public ImmutableAuditableThing(Long id, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, + String modifiedBy, String name) { this.id = id; this.createdAt = createdAt; this.createdBy = createdBy; @@ -65,26 +71,62 @@ public final class ImmutableAuditableThing implements AuditableThing { return this.id; } + @Override public LocalDateTime getCreatedAt() { return this.createdAt; } + @Override public String getCreatedBy() { return this.createdBy; } + @Override public LocalDateTime getModifiedAt() { return this.modifiedAt; } + @Override public String getModifiedBy() { return this.modifiedBy; } + @Override public String getName() { return this.name; } + public ImmutableAuditableThing withId(Long id) { + return Objects.equals(this.id, id) ? this : new ImmutableAuditableThing(id, this.createdAt, this.createdBy, + this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThing withCreatedAt(LocalDateTime createdAt) { + return Objects.equals(this.createdAt, createdAt) ? this : new ImmutableAuditableThing(this.id, createdAt, + this.createdBy, this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThing withCreatedBy(String createdBy) { + return Objects.equals(this.createdBy, createdBy) ? this : new ImmutableAuditableThing(this.id, this.createdAt, + createdBy, this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThing withModifiedAt(LocalDateTime modifiedAt) { + return Objects.equals(this.modifiedAt, modifiedAt) ? this : new ImmutableAuditableThing(this.id, this.createdAt, + this.createdBy, modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThing withModifiedBy(String modifiedBy) { + return Objects.equals(this.modifiedBy, modifiedBy) ? this : new ImmutableAuditableThing(this.id, this.createdAt, + this.createdBy, this.modifiedAt, modifiedBy, this.name); + } + + public ImmutableAuditableThing withName(String name) { + return Objects.equals(this.name, name) ? this : new ImmutableAuditableThing(this.id, this.createdAt, + this.createdBy, this.modifiedAt, this.modifiedBy, name); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -95,80 +137,58 @@ public final class ImmutableAuditableThing implements AuditableThing { final ImmutableAuditableThing other = (ImmutableAuditableThing) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$createdAt = this.getCreatedAt(); final Object other$createdAt = other.getCreatedAt(); - if (this$createdAt == null ? other$createdAt != null : !this$createdAt.equals(other$createdAt)) { + if (!Objects.equals(this$createdAt, other$createdAt)) { return false; } final Object this$createdBy = this.getCreatedBy(); final Object other$createdBy = other.getCreatedBy(); - if (this$createdBy == null ? other$createdBy != null : !this$createdBy.equals(other$createdBy)) { + if (!Objects.equals(this$createdBy, other$createdBy)) { return false; } final Object this$modifiedAt = this.getModifiedAt(); final Object other$modifiedAt = other.getModifiedAt(); - if (this$modifiedAt == null ? other$modifiedAt != null : !this$modifiedAt.equals(other$modifiedAt)) { + if (!Objects.equals(this$modifiedAt, other$modifiedAt)) { return false; } final Object this$modifiedBy = this.getModifiedBy(); final Object other$modifiedBy = other.getModifiedBy(); - if (this$modifiedBy == null ? other$modifiedBy != null : !this$modifiedBy.equals(other$modifiedBy)) { + if (!Objects.equals(this$modifiedBy, other$modifiedBy)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $createdAt = this.getCreatedAt(); - result = result * PRIME + ($createdAt == null ? 43 : $createdAt.hashCode()); + result = result * PRIME + (($createdAt != null) ? $createdAt.hashCode() : 43); final Object $createdBy = this.getCreatedBy(); - result = result * PRIME + ($createdBy == null ? 43 : $createdBy.hashCode()); + result = result * PRIME + (($createdBy != null) ? $createdBy.hashCode() : 43); final Object $modifiedAt = this.getModifiedAt(); - result = result * PRIME + ($modifiedAt == null ? 43 : $modifiedAt.hashCode()); + result = result * PRIME + (($modifiedAt != null) ? $modifiedAt.hashCode() : 43); final Object $modifiedBy = this.getModifiedBy(); - result = result * PRIME + ($modifiedBy == null ? 43 : $modifiedBy.hashCode()); + result = result * PRIME + (($modifiedBy != null) ? $modifiedBy.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); return result; } + @Override public String toString() { - return "ImmutableAuditableThing(id=" + this.getId() + ", createdAt=" + this.getCreatedAt() + ", createdBy=" + this.getCreatedBy() + ", modifiedAt=" + this.getModifiedAt() + ", modifiedBy=" + this.getModifiedBy() + ", name=" + this.getName() + ")"; + return "ImmutableAuditableThing(id=" + this.getId() + ", createdAt=" + this.getCreatedAt() + ", createdBy=" + + this.getCreatedBy() + ", modifiedAt=" + this.getModifiedAt() + ", modifiedBy=" + this.getModifiedBy() + + ", name=" + this.getName() + ")"; } - public ImmutableAuditableThing withId(Long id) { - return this.id == id ? this : new ImmutableAuditableThing(id, this.createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThing withCreatedAt(LocalDateTime createdAt) { - return this.createdAt == createdAt ? this : new ImmutableAuditableThing(this.id, createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThing withCreatedBy(String createdBy) { - return this.createdBy == createdBy ? this : new ImmutableAuditableThing(this.id, this.createdAt, createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThing withModifiedAt(LocalDateTime modifiedAt) { - return this.modifiedAt == modifiedAt ? this : new ImmutableAuditableThing(this.id, this.createdAt, this.createdBy, modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThing withModifiedBy(String modifiedBy) { - return this.modifiedBy == modifiedBy ? this : new ImmutableAuditableThing(this.id, this.createdAt, this.createdBy, this.modifiedAt, modifiedBy, this.name); - } - - public ImmutableAuditableThing withName(String name) { - return this.name == name ? this : new ImmutableAuditableThing(this.id, this.createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, name); - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThingWithGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThingWithGeneratedId.java index 916020107..8b8e86727 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThingWithGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableAuditableThingWithGeneratedId.java @@ -15,6 +15,9 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.time.LocalDateTime; +import java.util.Objects; + import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; @@ -25,8 +28,6 @@ import org.springframework.data.annotation.Persistent; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.support.UUIDStringGenerator; -import java.time.LocalDateTime; - /** * @author Michael J. Simons */ @@ -37,12 +38,16 @@ public final class ImmutableAuditableThingWithGeneratedId implements AuditableTh @Id @GeneratedValue(UUIDStringGenerator.class) private final String id; + @CreatedDate private final LocalDateTime createdAt; + @CreatedBy private final String createdBy; + @LastModifiedDate private final LocalDateTime modifiedAt; + @LastModifiedBy private final String modifiedBy; @@ -53,7 +58,8 @@ public final class ImmutableAuditableThingWithGeneratedId implements AuditableTh } @PersistenceCreator - public ImmutableAuditableThingWithGeneratedId(String id, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy, String name) { + public ImmutableAuditableThingWithGeneratedId(String id, LocalDateTime createdAt, String createdBy, + LocalDateTime modifiedAt, String modifiedBy, String name) { this.id = id; this.createdAt = createdAt; this.createdBy = createdBy; @@ -66,26 +72,62 @@ public final class ImmutableAuditableThingWithGeneratedId implements AuditableTh return this.id; } + @Override public LocalDateTime getCreatedAt() { return this.createdAt; } + @Override public String getCreatedBy() { return this.createdBy; } + @Override public LocalDateTime getModifiedAt() { return this.modifiedAt; } + @Override public String getModifiedBy() { return this.modifiedBy; } + @Override public String getName() { return this.name; } + public ImmutableAuditableThingWithGeneratedId withId(String id) { + return Objects.equals(this.id, id) ? this : new ImmutableAuditableThingWithGeneratedId(id, this.createdAt, + this.createdBy, this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThingWithGeneratedId withCreatedAt(LocalDateTime createdAt) { + return Objects.equals(this.createdAt, createdAt) ? this : new ImmutableAuditableThingWithGeneratedId(this.id, + createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThingWithGeneratedId withCreatedBy(String createdBy) { + return Objects.equals(this.createdBy, createdBy) ? this : new ImmutableAuditableThingWithGeneratedId(this.id, + this.createdAt, createdBy, this.modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThingWithGeneratedId withModifiedAt(LocalDateTime modifiedAt) { + return Objects.equals(this.modifiedAt, modifiedAt) ? this : new ImmutableAuditableThingWithGeneratedId(this.id, + this.createdAt, this.createdBy, modifiedAt, this.modifiedBy, this.name); + } + + public ImmutableAuditableThingWithGeneratedId withModifiedBy(String modifiedBy) { + return Objects.equals(this.modifiedBy, modifiedBy) ? this : new ImmutableAuditableThingWithGeneratedId(this.id, + this.createdAt, this.createdBy, this.modifiedAt, modifiedBy, this.name); + } + + public ImmutableAuditableThingWithGeneratedId withName(String name) { + return Objects.equals(this.name, name) ? this : new ImmutableAuditableThingWithGeneratedId(this.id, + this.createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, name); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -96,80 +138,58 @@ public final class ImmutableAuditableThingWithGeneratedId implements AuditableTh final ImmutableAuditableThingWithGeneratedId other = (ImmutableAuditableThingWithGeneratedId) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$createdAt = this.getCreatedAt(); final Object other$createdAt = other.getCreatedAt(); - if (this$createdAt == null ? other$createdAt != null : !this$createdAt.equals(other$createdAt)) { + if (!Objects.equals(this$createdAt, other$createdAt)) { return false; } final Object this$createdBy = this.getCreatedBy(); final Object other$createdBy = other.getCreatedBy(); - if (this$createdBy == null ? other$createdBy != null : !this$createdBy.equals(other$createdBy)) { + if (!Objects.equals(this$createdBy, other$createdBy)) { return false; } final Object this$modifiedAt = this.getModifiedAt(); final Object other$modifiedAt = other.getModifiedAt(); - if (this$modifiedAt == null ? other$modifiedAt != null : !this$modifiedAt.equals(other$modifiedAt)) { + if (!Objects.equals(this$modifiedAt, other$modifiedAt)) { return false; } final Object this$modifiedBy = this.getModifiedBy(); final Object other$modifiedBy = other.getModifiedBy(); - if (this$modifiedBy == null ? other$modifiedBy != null : !this$modifiedBy.equals(other$modifiedBy)) { + if (!Objects.equals(this$modifiedBy, other$modifiedBy)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $createdAt = this.getCreatedAt(); - result = result * PRIME + ($createdAt == null ? 43 : $createdAt.hashCode()); + result = result * PRIME + (($createdAt != null) ? $createdAt.hashCode() : 43); final Object $createdBy = this.getCreatedBy(); - result = result * PRIME + ($createdBy == null ? 43 : $createdBy.hashCode()); + result = result * PRIME + (($createdBy != null) ? $createdBy.hashCode() : 43); final Object $modifiedAt = this.getModifiedAt(); - result = result * PRIME + ($modifiedAt == null ? 43 : $modifiedAt.hashCode()); + result = result * PRIME + (($modifiedAt != null) ? $modifiedAt.hashCode() : 43); final Object $modifiedBy = this.getModifiedBy(); - result = result * PRIME + ($modifiedBy == null ? 43 : $modifiedBy.hashCode()); + result = result * PRIME + (($modifiedBy != null) ? $modifiedBy.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); return result; } + @Override public String toString() { - return "ImmutableAuditableThingWithGeneratedId(id=" + this.getId() + ", createdAt=" + this.getCreatedAt() + ", createdBy=" + this.getCreatedBy() + ", modifiedAt=" + this.getModifiedAt() + ", modifiedBy=" + this.getModifiedBy() + ", name=" + this.getName() + ")"; + return "ImmutableAuditableThingWithGeneratedId(id=" + this.getId() + ", createdAt=" + this.getCreatedAt() + + ", createdBy=" + this.getCreatedBy() + ", modifiedAt=" + this.getModifiedAt() + ", modifiedBy=" + + this.getModifiedBy() + ", name=" + this.getName() + ")"; } - public ImmutableAuditableThingWithGeneratedId withId(String id) { - return this.id == id ? this : new ImmutableAuditableThingWithGeneratedId(id, this.createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThingWithGeneratedId withCreatedAt(LocalDateTime createdAt) { - return this.createdAt == createdAt ? this : new ImmutableAuditableThingWithGeneratedId(this.id, createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThingWithGeneratedId withCreatedBy(String createdBy) { - return this.createdBy == createdBy ? this : new ImmutableAuditableThingWithGeneratedId(this.id, this.createdAt, createdBy, this.modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThingWithGeneratedId withModifiedAt(LocalDateTime modifiedAt) { - return this.modifiedAt == modifiedAt ? this : new ImmutableAuditableThingWithGeneratedId(this.id, this.createdAt, this.createdBy, modifiedAt, this.modifiedBy, this.name); - } - - public ImmutableAuditableThingWithGeneratedId withModifiedBy(String modifiedBy) { - return this.modifiedBy == modifiedBy ? this : new ImmutableAuditableThingWithGeneratedId(this.id, this.createdAt, this.createdBy, this.modifiedAt, modifiedBy, this.name); - } - - public ImmutableAuditableThingWithGeneratedId withName(String name) { - return this.name == name ? this : new ImmutableAuditableThingWithGeneratedId(this.id, this.createdAt, this.createdBy, this.modifiedAt, this.modifiedBy, name); - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePerson.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePerson.java index 8bdb084a2..92398ded4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePerson.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePerson.java @@ -29,8 +29,10 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class ImmutablePerson { + @Id private final String name; + private final List wasOnboardedBy; public ImmutablePerson(String name, List wasOnboardedBy) { @@ -39,10 +41,11 @@ public class ImmutablePerson { } public String getName() { - return name; + return this.name; } public List getWasOnboardedBy() { - return wasOnboardedBy; + return this.wasOnboardedBy; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedId.java index c4075fab0..7bfe62a36 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedId.java @@ -15,17 +15,17 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.springframework.data.annotation.PersistenceCreator; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Relationship; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + /** * @author Gerrit Meier */ @@ -35,14 +35,14 @@ public class ImmutablePersonWithAssignedId { @Id public final Long id; - public String someValue; - @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -55,14 +55,14 @@ public class ImmutablePersonWithAssignedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; + public String someValue; + @PersistenceCreator - public ImmutablePersonWithAssignedId( - Long id, - List wasOnboardedBy, - Set knownBy, - Map ratedBy, + public ImmutablePersonWithAssignedId(Long id, List wasOnboardedBy, + Set knownBy, Map ratedBy, Map> ratedByCollection, ImmutablePersonWithAssignedId fallback, ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties, @@ -83,143 +83,67 @@ public class ImmutablePersonWithAssignedId { } public ImmutablePersonWithAssignedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } public static ImmutablePersonWithAssignedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutablePersonWithAssignedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithAssignedId(null, wasOnboardedBy, Collections.emptySet(), Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithAssignedId knownBy(Set knownBy) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), knownBy, Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithAssignedId ratedBy(Map ratedBy) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), ratedBy, + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutablePersonWithAssignedId ratedByCollection(Map> ratedByCollection) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithAssignedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithAssignedId fallback(ImmutablePersonWithAssignedId fallback) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithAssignedId relationshipProperties(ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithAssignedId relationshipProperties( + ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties) { + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithAssignedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithAssignedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithAssignedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutablePersonWithAssignedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutablePersonWithAssignedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutablePersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutablePersonWithAssignedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutablePersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedIdRelationshipProperties.java index da4e7ec76..a1cd4b97b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithAssignedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutablePersonWithAssignedIdRelationshipProperties { @TargetNode public final ImmutablePersonWithAssignedId target; - public ImmutablePersonWithAssignedIdRelationshipProperties(Long id, String name, ImmutablePersonWithAssignedId target) { + public ImmutablePersonWithAssignedIdRelationshipProperties(Long id, String name, + ImmutablePersonWithAssignedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedId.java index 58fc75204..348799493 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedId.java @@ -15,18 +15,18 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.springframework.data.annotation.PersistenceCreator; -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.Relationship; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.springframework.data.annotation.PersistenceCreator; +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.Relationship; + /** * @author Gerrit Meier */ @@ -39,10 +39,12 @@ public class ImmutablePersonWithExternallyGeneratedId { @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -55,11 +57,11 @@ public class ImmutablePersonWithExternallyGeneratedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; @PersistenceCreator - public ImmutablePersonWithExternallyGeneratedId( - UUID id, + public ImmutablePersonWithExternallyGeneratedId(UUID id, List wasOnboardedBy, Set knownBy, Map ratedBy, @@ -83,143 +85,70 @@ public class ImmutablePersonWithExternallyGeneratedId { } public ImmutablePersonWithExternallyGeneratedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutablePersonWithExternallyGeneratedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId wasOnboardedBy( + List wasOnboardedBy) { + return new ImmutablePersonWithExternallyGeneratedId(null, wasOnboardedBy, Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId knownBy(Set knownBy) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId knownBy( + Set knownBy) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), knownBy, + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId ratedBy(Map ratedBy) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId ratedBy( + Map ratedBy) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + ratedBy, Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId ratedByCollection(Map> ratedByCollection) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithExternallyGeneratedId fallback(ImmutablePersonWithExternallyGeneratedId fallback) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId relationshipProperties(ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId relationshipProperties( + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutablePersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutablePersonWithExternallyGeneratedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutablePersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedIdRelationshipProperties.java index 3d802e01a..68e720ea2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithExternallyGeneratedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutablePersonWithExternallyGeneratedIdRelationshipProperties { @TargetNode public final ImmutablePersonWithExternallyGeneratedId target; - public ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(Long id, String name, ImmutablePersonWithExternallyGeneratedId target) { + public ImmutablePersonWithExternallyGeneratedIdRelationshipProperties(Long id, String name, + ImmutablePersonWithExternallyGeneratedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedId.java index 0f54a73b7..3a68fab79 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedId.java @@ -15,17 +15,17 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.springframework.data.annotation.PersistenceCreator; 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.Relationship; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - /** * @author Gerrit Meier */ @@ -38,10 +38,12 @@ public class ImmutablePersonWithGeneratedId { @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -54,14 +56,12 @@ public class ImmutablePersonWithGeneratedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; @PersistenceCreator - public ImmutablePersonWithGeneratedId( - Long id, - List wasOnboardedBy, - Set knownBy, - Map ratedBy, + public ImmutablePersonWithGeneratedId(Long id, List wasOnboardedBy, + Set knownBy, Map ratedBy, Map> ratedByCollection, ImmutablePersonWithGeneratedId fallback, ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties, @@ -82,143 +82,67 @@ public class ImmutablePersonWithGeneratedId { } public ImmutablePersonWithGeneratedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } public static ImmutablePersonWithGeneratedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutablePersonWithGeneratedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithGeneratedId(null, wasOnboardedBy, Collections.emptySet(), Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithGeneratedId knownBy(Set knownBy) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), knownBy, Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithGeneratedId ratedBy(Map ratedBy) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), ratedBy, + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutablePersonWithGeneratedId ratedByCollection(Map> ratedByCollection) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithGeneratedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutablePersonWithGeneratedId fallback(ImmutablePersonWithGeneratedId fallback) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithGeneratedId relationshipProperties(ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithGeneratedId relationshipProperties( + ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties) { + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithGeneratedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutablePersonWithGeneratedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutablePersonWithGeneratedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutablePersonWithGeneratedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutablePersonWithGeneratedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutablePersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutablePersonWithGeneratedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutablePersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedIdRelationshipProperties.java index 3bb209aee..4f7c47214 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePersonWithGeneratedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutablePersonWithGeneratedIdRelationshipProperties { @TargetNode public final ImmutablePersonWithGeneratedId target; - public ImmutablePersonWithGeneratedIdRelationshipProperties(Long id, String name, ImmutablePersonWithGeneratedId target) { + public ImmutablePersonWithGeneratedIdRelationshipProperties(Long id, String name, + ImmutablePersonWithGeneratedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePet.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePet.java index c3c96e577..88b6e1d4d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePet.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutablePet.java @@ -15,14 +15,14 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Set; + import org.springframework.data.annotation.PersistenceCreator; 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.Relationship; -import java.util.Set; - /** * @author Gerrit Meier */ @@ -52,6 +52,7 @@ public class ImmutablePet { } public ImmutablePet withFriends(Set newFriends) { - return new ImmutablePet(id, name, newFriends); + return new ImmutablePet(this.id, this.name, newFriends); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedId.java index 82f8aa040..469f58ff0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedId.java @@ -15,17 +15,17 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.springframework.data.annotation.PersistenceCreator; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Relationship; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + /** * @author Gerrit Meier */ @@ -37,10 +37,12 @@ public class ImmutableSecondPersonWithAssignedId { @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -53,14 +55,12 @@ public class ImmutableSecondPersonWithAssignedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; @PersistenceCreator - public ImmutableSecondPersonWithAssignedId( - Long id, - List wasOnboardedBy, - Set knownBy, - Map ratedBy, + public ImmutableSecondPersonWithAssignedId(Long id, List wasOnboardedBy, + Set knownBy, Map ratedBy, Map> ratedByCollection, ImmutableSecondPersonWithAssignedId fallback, ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties, @@ -81,143 +81,68 @@ public class ImmutableSecondPersonWithAssignedId { } public ImmutableSecondPersonWithAssignedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutableSecondPersonWithAssignedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithAssignedId wasOnboardedBy( + List wasOnboardedBy) { + return new ImmutableSecondPersonWithAssignedId(null, wasOnboardedBy, Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } public static ImmutableSecondPersonWithAssignedId knownBy(Set knownBy) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), knownBy, Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutableSecondPersonWithAssignedId ratedBy(Map ratedBy) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), ratedBy, + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId ratedByCollection(Map> ratedByCollection) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithAssignedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutableSecondPersonWithAssignedId fallback(ImmutableSecondPersonWithAssignedId fallback) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId relationshipProperties(ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithAssignedId relationshipProperties( + ImmutablePersonWithAssignedIdRelationshipProperties relationshipProperties) { + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithAssignedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithAssignedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutableSecondPersonWithAssignedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutableSecondPersonWithAssignedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutableSecondPersonWithAssignedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutableSecondPersonWithAssignedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedIdRelationshipProperties.java index fa72304b6..d0899de3f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithAssignedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutableSecondPersonWithAssignedIdRelationshipProperties { @TargetNode public final ImmutableSecondPersonWithAssignedId target; - public ImmutableSecondPersonWithAssignedIdRelationshipProperties(Long id, String name, ImmutableSecondPersonWithAssignedId target) { + public ImmutableSecondPersonWithAssignedIdRelationshipProperties(Long id, String name, + ImmutableSecondPersonWithAssignedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedId.java index 1689b2489..66ce506b6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedId.java @@ -15,18 +15,18 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.springframework.data.annotation.PersistenceCreator; -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.Relationship; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.springframework.data.annotation.PersistenceCreator; +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.Relationship; + /** * @author Gerrit Meier */ @@ -39,10 +39,12 @@ public class ImmutableSecondPersonWithExternallyGeneratedId { @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -55,11 +57,11 @@ public class ImmutableSecondPersonWithExternallyGeneratedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; @PersistenceCreator - public ImmutableSecondPersonWithExternallyGeneratedId( - UUID id, + public ImmutableSecondPersonWithExternallyGeneratedId(UUID id, List wasOnboardedBy, Set knownBy, Map ratedBy, @@ -83,143 +85,71 @@ public class ImmutableSecondPersonWithExternallyGeneratedId { } public ImmutableSecondPersonWithExternallyGeneratedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId wasOnboardedBy( + List wasOnboardedBy) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, wasOnboardedBy, Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId knownBy(Set knownBy) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId knownBy( + Set knownBy) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), knownBy, + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId ratedBy(Map ratedBy) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId ratedBy( + Map ratedBy) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + ratedBy, Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId ratedByCollection(Map> ratedByCollection) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId fallback(ImmutableSecondPersonWithExternallyGeneratedId fallback) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId fallback( + ImmutableSecondPersonWithExternallyGeneratedId fallback) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId relationshipProperties(ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId relationshipProperties( + ImmutablePersonWithExternallyGeneratedIdRelationshipProperties relationshipProperties) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutableSecondPersonWithExternallyGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutableSecondPersonWithExternallyGeneratedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutableSecondPersonWithExternallyGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties.java index 039eef6f1..491c64537 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutableSecondPersonWithExternallyGeneratedIdRelationshipPropertie @TargetNode public final ImmutableSecondPersonWithExternallyGeneratedId target; - public ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties(Long id, String name, ImmutableSecondPersonWithExternallyGeneratedId target) { + public ImmutableSecondPersonWithExternallyGeneratedIdRelationshipProperties(Long id, String name, + ImmutableSecondPersonWithExternallyGeneratedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedId.java index c035d9a2c..7d3664e51 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedId.java @@ -15,32 +15,35 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.springframework.data.annotation.PersistenceCreator; 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.Relationship; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - /** * @author Gerrit Meier */ @Node public class ImmutableSecondPersonWithGeneratedId { + @Id @GeneratedValue public final Long id; @Relationship("ONBOARDED_BY") public final List wasOnboardedBy; + @Relationship("KNOWN_BY") public final Set knownBy; public final Map ratedBy; + public final Map> ratedByCollection; @Relationship("FALLBACK") @@ -53,14 +56,12 @@ public class ImmutableSecondPersonWithGeneratedId { public final List relationshipPropertiesCollection; public final Map relationshipPropertiesDynamic; + public final Map> relationshipPropertiesDynamicCollection; @PersistenceCreator - public ImmutableSecondPersonWithGeneratedId( - Long id, - List wasOnboardedBy, - Set knownBy, - Map ratedBy, + public ImmutableSecondPersonWithGeneratedId(Long id, List wasOnboardedBy, + Set knownBy, Map ratedBy, Map> ratedByCollection, ImmutableSecondPersonWithGeneratedId fallback, ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties, @@ -81,142 +82,68 @@ public class ImmutableSecondPersonWithGeneratedId { } public ImmutableSecondPersonWithGeneratedId() { - this(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + this(null, Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + null, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId wasOnboardedBy(List wasOnboardedBy) { - return new ImmutableSecondPersonWithGeneratedId(null, - wasOnboardedBy, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithGeneratedId wasOnboardedBy( + List wasOnboardedBy) { + return new ImmutableSecondPersonWithGeneratedId(null, wasOnboardedBy, Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } public static ImmutableSecondPersonWithGeneratedId knownBy(Set knownBy) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - knownBy, - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), knownBy, Collections.emptyMap(), + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutableSecondPersonWithGeneratedId ratedBy(Map ratedBy) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - ratedBy, - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), ratedBy, + Collections.emptyMap(), null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId ratedByCollection(Map> ratedByCollection) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - ratedByCollection, - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithGeneratedId ratedByCollection( + Map> ratedByCollection) { + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), ratedByCollection, null, null, Collections.emptyList(), Collections.emptyMap(), + Collections.emptyMap()); } public static ImmutableSecondPersonWithGeneratedId fallback(ImmutableSecondPersonWithGeneratedId fallback) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - fallback, - null, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), fallback, null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId relationshipProperties(ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - relationshipProperties, - Collections.emptyList(), - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithGeneratedId relationshipProperties( + ImmutablePersonWithGeneratedIdRelationshipProperties relationshipProperties) { + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, relationshipProperties, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesCollection(List relationshipPropertiesCollection) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - relationshipPropertiesCollection, - Collections.emptyMap(), - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesCollection( + List relationshipPropertiesCollection) { + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, relationshipPropertiesCollection, + Collections.emptyMap(), Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesDynamic(Map relationshipPropertiesDynamic) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - relationshipPropertiesDynamic, - Collections.emptyMap() - ); + public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesDynamic( + Map relationshipPropertiesDynamic) { + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + relationshipPropertiesDynamic, Collections.emptyMap()); } - public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesDynamicCollection(Map> relationshipPropertiesDynamicCollection) { - return new ImmutableSecondPersonWithGeneratedId(null, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptyMap(), - null, - null, - Collections.emptyList(), - Collections.emptyMap(), - relationshipPropertiesDynamicCollection - ); + public static ImmutableSecondPersonWithGeneratedId relationshipPropertiesDynamicCollection( + Map> relationshipPropertiesDynamicCollection) { + return new ImmutableSecondPersonWithGeneratedId(null, Collections.emptyList(), Collections.emptySet(), + Collections.emptyMap(), Collections.emptyMap(), null, null, Collections.emptyList(), + Collections.emptyMap(), relationshipPropertiesDynamicCollection); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedIdRelationshipProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedIdRelationshipProperties.java index 6795b6e8a..c2dfc4ce1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedIdRelationshipProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableSecondPersonWithGeneratedIdRelationshipProperties.java @@ -33,9 +33,11 @@ public class ImmutableSecondPersonWithGeneratedIdRelationshipProperties { @TargetNode public final ImmutableSecondPersonWithGeneratedId target; - public ImmutableSecondPersonWithGeneratedIdRelationshipProperties(Long id, String name, ImmutableSecondPersonWithGeneratedId target) { + public ImmutableSecondPersonWithGeneratedIdRelationshipProperties(Long id, String name, + ImmutableSecondPersonWithGeneratedId target) { this.id = id; this.name = name; this.target = target; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableVersionedThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableVersionedThing.java index ee3f9d0fb..0d5372e64 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableVersionedThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ImmutableVersionedThing.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + import org.springframework.data.annotation.Version; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -57,10 +59,12 @@ public class ImmutableVersionedThing { } public ImmutableVersionedThing withMyVersion(Long myVersion) { - return this.myVersion == myVersion ? this : new ImmutableVersionedThing(this.id, myVersion, this.name); + return Objects.equals(this.myVersion, myVersion) ? this + : new ImmutableVersionedThing(this.id, myVersion, this.name); } public ImmutableVersionedThing withName(String name) { - return this.name == name ? this : new ImmutableVersionedThing(this.id, this.myVersion, name); + return Objects.equals(this.name, name) ? this : new ImmutableVersionedThing(this.id, this.myVersion, name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Inheritance.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Inheritance.java index 3a3e887f8..a2f012dda 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Inheritance.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Inheritance.java @@ -37,29 +37,81 @@ public class Inheritance { /** * An interface as someone would define in an api package */ - // tag::interface1[] - public interface SomeInterface { // <.> + public interface SomeInterface { String getName(); SomeInterface getRelated(); + } - // end::interface1[] + /** + * A case where the label was specified on the interface, unsure if this is meaningful + */ + @Node("PrimaryLabelWN") + public interface SomeInterface2 { + + String getName(); + + SomeInterface2 getRelated(); + + } + + /** + * Concrete interface name here, `@Node` is required, label can be omitted in that + * case + */ + @Node("SomeInterface3") + public interface SomeInterface3 { + + String getName(); + + SomeInterface3 getRelated(); + + } + + /** + * Interface to get implemented with the one below. + */ + @Node("Mix1") + public interface MixIt1 { + + String getName(); + + } + + /** + * Interface to get implemented with one above. + */ + @Node("Mix2") + public interface MixIt2 { + + String getValue(); + + } + + /** + * Interface for relationship + */ + @Node("GH-2788-Interface") + public interface Gh2788Interface { + + String getName(); + + } /** * Implementation of the above, to be found in a Neo4j or Mongo or whatever module. */ - // tag::interface1[] - @Node("SomeInterface") // <.> + @Node("SomeInterface") public static class SomeInterfaceEntity implements SomeInterface { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - private SomeInterface related; public SomeInterfaceEntity(String name) { @@ -68,55 +120,37 @@ public class Inheritance { @Override public String getName() { - return name; + return this.name; } @Override public SomeInterface getRelated() { - return related; - } - // end::interface1[] - - public Long getId() { - return id; + return this.related; } public void setRelated(SomeInterface related) { this.related = related; } - // tag::interface1[] + public Long getId() { + return this.id; + } + } - // end::interface1[] - - /** - * A case where the label was specified on the interface, unsure if this is meaningful - */ - // tag::interface2[] - @Node("PrimaryLabelWN") // <.> - public interface SomeInterface2 { - - String getName(); - - SomeInterface2 getRelated(); - } - - // end::interface2[] /** * Implementation of the above */ - // tag::interface2[] public static class SomeInterfaceEntity2 implements SomeInterface2 { + private final String name; + // Overrides omitted for brevity - // end::interface2[] + @Id @GeneratedValue private Long id; - private final String name; - private SomeInterface2 related; public SomeInterfaceEntity2(String name) { @@ -124,136 +158,114 @@ public class Inheritance { } public Long getId() { - return id; + return this.id; } @Override public String getName() { - return name; + return this.name; } @Override public SomeInterface2 getRelated() { - return related; + return this.related; } public void setRelated(SomeInterface2 related) { this.related = related; } - // tag::interface2[] + } - // end::interface2[] - - /** - * Concrete interface name here, `@Node` is required, label can be omitted in that case - */ - // tag::interface3[] - @Node("SomeInterface3") // <.> - public interface SomeInterface3 { - - String getName(); - - SomeInterface3 getRelated(); - } - - // end::interface3[] /** * One implementation of the above. */ - // tag::interface3[] - @Node("SomeInterface3a") // <.> + + @Node("SomeInterface3a") public static class SomeInterfaceImpl3a implements SomeInterface3 { + private final String name; + // Overrides omitted for brevity - // end::interface3[] + @Id @GeneratedValue private Long id; private SomeInterfaceImpl3b related; - private final String name; - public SomeInterfaceImpl3a(String name) { this.name = name; } @Override public SomeInterface3 getRelated() { - return related; + return this.related; } @Override public String getName() { - return name; + return this.name; } - // tag::interface3[] } - // end::interface3[] /** * Another implementation of the above. */ - // tag::interface3[] - @Node("SomeInterface3b") // <.> + + @Node("SomeInterface3b") public static class SomeInterfaceImpl3b implements SomeInterface3 { + private final String name; + // Overrides omitted for brevity - // end::interface3[] + @Id @GeneratedValue private Long id; private SomeInterfaceImpl3a related; - private final String name; - public SomeInterfaceImpl3b(String name) { this.name = name; } @Override public SomeInterface3 getRelated() { - return related; + return this.related; } @Override public String getName() { - return name; + return this.name; } - // tag::interface3[] } - // end::interface3[] - - /** * A thing having different relationships with the same type. */ - // tag::interface3[] + @Node - public static class ParentModel { // <.> + public static class ParentModel { + + private final String name; @Id @GeneratedValue private Long id; - private SomeInterface3 related1; // <.> + private SomeInterface3 related1; private SomeInterface3 related2; - // end::interface3[] - - private final String name; public ParentModel(String name) { this.name = name; } public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -261,11 +273,11 @@ public class Inheritance { } public String getName() { - return name; + return this.name; } public SomeInterface3 getRelated1() { - return related1; + return this.related1; } public void setRelated1(SomeInterface3 related1) { @@ -273,16 +285,14 @@ public class Inheritance { } public SomeInterface3 getRelated2() { - return related2; + return this.related2; } public void setRelated2(SomeInterface3 related2) { this.related2 = related2; } - // tag::interface3[] } - // end::interface3[] /** * A holder for a list of different interface implementations, see GH-2262. @@ -297,32 +307,17 @@ public class Inheritance { private List isRelatedTo; public Long getId() { - return id; + return this.id; } public List getIsRelatedTo() { - return isRelatedTo; + return this.isRelatedTo; } public void setIsRelatedTo(List isRelatedTo) { this.isRelatedTo = isRelatedTo; } - } - /** - * Interface to get implemented with the one below. - */ - @Node("Mix1") - public interface MixIt1 { - String getName(); - } - - /** - * Interface to get implemented with one above. - */ - @Node("Mix2") - public interface MixIt2 { - String getValue(); } /** @@ -331,13 +326,14 @@ public class Inheritance { @Node public static class Mix1AndMix2 implements MixIt1, MixIt2 { + private final String name; + + private final String value; + @Id @GeneratedValue private Long id; - private final String name; - private final String value; - public Mix1AndMix2(String name, String value) { this.name = name; this.value = value; @@ -345,34 +341,37 @@ public class Inheritance { @Override public String getName() { - return name; + return this.name; } @Override public String getValue() { - return value; + return this.value; } + } /** * super base class */ @Node - public static abstract class SuperBaseClass { + public abstract static class SuperBaseClass { + @Id @GeneratedValue private Long id; public Long getId() { - return id; + return this.id; } + } /** * base class */ @Node - public static abstract class BaseClass extends SuperBaseClass { + public abstract static class BaseClass extends SuperBaseClass { private final String name; @@ -381,8 +380,9 @@ public class Inheritance { } public String getName() { - return name; + return this.name; } + } /** @@ -402,7 +402,7 @@ public class Inheritance { } public String getConcreteSomething() { - return concreteSomething; + return this.concreteSomething; } @Override @@ -414,13 +414,14 @@ public class Inheritance { return false; } ConcreteClassA that = (ConcreteClassA) o; - return concreteSomething.equals(that.concreteSomething); + return this.concreteSomething.equals(that.concreteSomething); } @Override public int hashCode() { - return Objects.hash(concreteSomething); + return Objects.hash(this.concreteSomething); } + } /** @@ -445,30 +446,32 @@ public class Inheritance { return false; } ConcreteClassB that = (ConcreteClassB) o; - return age.equals(that.age); + return this.age.equals(that.age); } @Override public int hashCode() { - return Objects.hash(age); + return Objects.hash(this.age); } + } /** * Base class with explicit primary and additional labels. */ - @Node({"LabeledBaseClass", "And_another_one"}) - public static abstract class BaseClassWithLabels { + @Node({ "LabeledBaseClass", "And_another_one" }) + public abstract static class BaseClassWithLabels { @Id @GeneratedValue private Long id; + } /** * Class that also has explicit labels */ - @Node({"ExtendingClassA", "And_yet_more_labels"}) + @Node({ "ExtendingClassA", "And_yet_more_labels" }) public static class ExtendingClassWithLabelsA extends BaseClassWithLabels { private final String name; @@ -478,7 +481,7 @@ public class Inheritance { } public String getName() { - return name; + return this.name; } @Override @@ -490,19 +493,20 @@ public class Inheritance { return false; } ExtendingClassWithLabelsA that = (ExtendingClassWithLabelsA) o; - return name.equals(that.name); + return this.name.equals(that.name); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(this.name); } + } /** * Another class that also has explicit labels */ - @Node({"ExtendingClassB", "And_other_labels"}) + @Node({ "ExtendingClassB", "And_other_labels" }) public static class ExtendingClassWithLabelsB extends BaseClassWithLabels { private final String somethingElse; @@ -512,7 +516,7 @@ public class Inheritance { } public String getSomethingElse() { - return somethingElse; + return this.somethingElse; } @Override @@ -524,13 +528,14 @@ public class Inheritance { return false; } ExtendingClassWithLabelsB that = (ExtendingClassWithLabelsB) o; - return somethingElse.equals(that.somethingElse); + return this.somethingElse.equals(that.somethingElse); } @Override public int hashCode() { - return Objects.hash(somethingElse); + return Objects.hash(this.somethingElse); } + } /** @@ -546,20 +551,22 @@ public class Inheritance { @Relationship("HAS") private List things; + public List getThings() { + return this.things; + } + public void setThings(List things) { this.things = things; } - public List getThings() { - return things; - } } /** * Abstract super base class with relationships */ @Node("SuperBaseClassWithRelationship") - public static abstract class SuperBaseClassWithRelationship { + public abstract static class SuperBaseClassWithRelationship { + @Id @GeneratedValue private Long id; @@ -567,33 +574,38 @@ public class Inheritance { @Relationship("RELATED_TO") private List boing; + public List getBoing() { + return this.boing; + } + public void setBoing(List boing) { this.boing = boing; } - public List getBoing() { - return boing; - } } /** * Abstract base class with relationships */ @Node("BaseClassWithRelationship") - public static abstract class BaseClassWithRelationship extends SuperBaseClassWithRelationship { + public abstract static class BaseClassWithRelationship extends SuperBaseClassWithRelationship { @Relationship("HAS") private List things; + public List getThings() { + return this.things; + } + public void setThings(List things) { this.things = things; } - public List getThings() { - return things; - } } + // Same as above but with relationship properties instead of direct relationship + // links. + /** * Concrete implementation */ @@ -603,22 +615,22 @@ public class Inheritance { @Relationship("SOMETHING_ELSE") private List somethingConcrete; + public List getSomethingConcrete() { + return this.somethingConcrete; + } + public void setSomethingConcrete(List somethingConcrete) { this.somethingConcrete = somethingConcrete; } - public List getSomethingConcrete() { - return somethingConcrete; - } } - // Same as above but with relationship properties instead of direct relationship links. - /** * Abstract super base class with relationship properties */ @Node("SuperBaseClassWithRelationshipProperties") - public static abstract class SuperBaseClassWithRelationshipProperties { + public abstract static class SuperBaseClassWithRelationshipProperties { + @Id @GeneratedValue private Long id; @@ -626,31 +638,33 @@ public class Inheritance { @Relationship("RELATED_TO") private List boing; + public List getBoing() { + return this.boing; + } + public void setBoing(List boing) { this.boing = boing; } - public List getBoing() { - return boing; - } } /** * Abstract base class with relationship properties */ @Node("BaseClassWithRelationshipProperties") - public static abstract class BaseClassWithRelationshipProperties extends SuperBaseClassWithRelationshipProperties { + public abstract static class BaseClassWithRelationshipProperties extends SuperBaseClassWithRelationshipProperties { @Relationship("HAS") private List things; + public List getThings() { + return this.things; + } + public void setThings(List things) { this.things = things; } - public List getThings() { - return things; - } } /** @@ -662,13 +676,14 @@ public class Inheritance { @Relationship("SOMETHING_ELSE") private List somethingConcrete; + public List getSomethingConcrete() { + return this.somethingConcrete; + } + public void setSomethingConcrete(List somethingConcrete) { this.somethingConcrete = somethingConcrete; } - public List getSomethingConcrete() { - return somethingConcrete; - } } /** @@ -696,13 +711,14 @@ public class Inheritance { return false; } ConcreteARelationshipProperties that = (ConcreteARelationshipProperties) o; - return target.equals(that.target); + return this.target.equals(that.target); } @Override public int hashCode() { - return Objects.hash(target); + return Objects.hash(this.target); } + } /** @@ -730,13 +746,14 @@ public class Inheritance { return false; } ConcreteBRelationshipProperties that = (ConcreteBRelationshipProperties) o; - return target.equals(that.target); + return this.target.equals(that.target); } @Override public int hashCode() { - return Objects.hash(target); + return Objects.hash(this.target); } + } /** @@ -764,27 +781,31 @@ public class Inheritance { return false; } SuperBaseClassRelationshipProperties that = (SuperBaseClassRelationshipProperties) o; - return target.equals(that.target); + return this.target.equals(that.target); } @Override public int hashCode() { - return Objects.hash(target); + return Objects.hash(this.target); } + } /** * Base entity for GH-2138 generic relationships tests */ @Node("Entity") - public static abstract class Entity { + public abstract static class Entity { + @org.springframework.data.annotation.Id @GeneratedValue public Long id; + public String name; @Relationship(type = "IS_CHILD") public Entity parent; + } /** @@ -792,6 +813,7 @@ public class Inheritance { */ @Node("Company") public static class Company extends Entity { + } /** @@ -799,6 +821,7 @@ public class Inheritance { */ @Node("Site") public static class Site extends Entity { + } /** @@ -806,33 +829,39 @@ public class Inheritance { */ @Node("Building") public static class Building extends Entity { + } /** * Base entity for GH-2138 generic relationship in child class tests */ @Node - public static abstract class BaseEntity { + public abstract static class BaseEntity { + @Id @GeneratedValue public Long id; + public String name; + } /** * BaseTerritory */ @Node - public static abstract class BaseTerritory extends BaseEntity { - public String nameEs; + public abstract static class BaseTerritory extends BaseEntity { + public final String nameEn; + public String nameEs; + public BaseTerritory(String nameEn) { this.nameEn = nameEn; } public String getNameEn() { - return nameEn; + return this.nameEn; } @Override @@ -844,13 +873,14 @@ public class Inheritance { return false; } BaseTerritory that = (BaseTerritory) o; - return nameEn.equals(that.nameEn); + return this.nameEn.equals(that.nameEn); } @Override public int hashCode() { - return Objects.hash(nameEn); + return Objects.hash(this.nameEn); } + } /** @@ -858,9 +888,11 @@ public class Inheritance { */ @Node public static class GenericTerritory extends BaseTerritory { + public GenericTerritory(String nameEn) { super(nameEn); } + } /** @@ -868,6 +900,7 @@ public class Inheritance { */ @Node public static class Country extends BaseTerritory { + public final String countryProperty; @Relationship(type = "LINK", direction = Relationship.Direction.OUTGOING) @@ -890,13 +923,14 @@ public class Inheritance { return false; } Country country = (Country) o; - return countryProperty.equals(country.countryProperty); + return this.countryProperty.equals(country.countryProperty); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), countryProperty); + return Objects.hash(super.hashCode(), this.countryProperty); } + } /** @@ -904,6 +938,7 @@ public class Inheritance { */ @Node public static class Continent extends BaseTerritory { + public final String continentProperty; public Continent(String nameEn, String continentProperty) { @@ -923,17 +958,19 @@ public class Inheritance { return false; } Continent continent = (Continent) o; - return continentProperty.equals(continent.continentProperty); + return this.continentProperty.equals(continent.continentProperty); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), continentProperty); + return Objects.hash(super.hashCode(), this.continentProperty); } + } /** - * A parent object for some territories, used to test whether those are loaded correct in a polymorphic way. + * A parent object for some territories, used to test whether those are loaded correct + * in a polymorphic way. */ @Node public static class Division extends BaseEntity { @@ -942,20 +979,20 @@ public class Inheritance { List isActiveIn; public List getIsActiveIn() { - return isActiveIn; + return this.isActiveIn; } - public void setIsActiveIn( - List isActiveIn) { + public void setIsActiveIn(List isActiveIn) { this.isActiveIn = isActiveIn; } + } /** * Parent class with relationship definition in the constructor */ @Node("PCWR") - public static abstract class ParentClassWithRelationship { + public abstract static class ParentClassWithRelationship { @Id @GeneratedValue @@ -968,6 +1005,7 @@ public class Inheritance { this.id = id; this.continent = continent; } + } /** @@ -981,24 +1019,22 @@ public class Inheritance { public ChildClassWithRelationship(Long id, Continent continent) { super(id, continent); } + } /** - * Entity that has an interface-based relationship. - * For testing that the properties and relationships of the implementing classes will also get fetched. + * Entity that has an interface-based relationship. For testing that the properties + * and relationships of the implementing classes will also get fetched. */ @Node("GH-2788-Entity") public static class Gh2788Entity { - @Id @GeneratedValue public String id; - public List relatedTo; - } - /** - * Interface for relationship - */ - @Node("GH-2788-Interface") - public interface Gh2788Interface { - String getName(); + @Id + @GeneratedValue + public String id; + + public List relatedTo; + } /** @@ -1006,12 +1042,17 @@ public class Inheritance { */ @Node("GH-2788-A") public static class Gh2788A implements Gh2788Interface { - @Id @GeneratedValue String id; public final String name; + public final String aValue; + public final List relatedTo; + @Id + @GeneratedValue + String id; + public Gh2788A(String name, String aValue, List relatedTo) { this.name = name; this.aValue = aValue; @@ -1020,8 +1061,9 @@ public class Inheritance { @Override public String getName() { - return name; + return this.name; } + } /** @@ -1029,7 +1071,11 @@ public class Inheritance { */ @Node public static class Gh2788ArelatedEntity { - @Id @GeneratedValue String id; + + @Id + @GeneratedValue + String id; + } /** @@ -1037,12 +1083,17 @@ public class Inheritance { */ @Node("GH-2788-B") public static class Gh2788B implements Gh2788Interface { - @Id @GeneratedValue String id; public final String name; + public final String bValue; + public final List relatedTo; + @Id + @GeneratedValue + String id; + public Gh2788B(String name, String bValue, List relatedTo) { this.name = name; this.bValue = bValue; @@ -1051,8 +1102,9 @@ public class Inheritance { @Override public String getName() { - return name; + return this.name; } + } /** @@ -1060,7 +1112,11 @@ public class Inheritance { */ @Node public static class Gh2788BrelatedEntity { - @Id @GeneratedValue String id; + + @Id + @GeneratedValue + String id; + } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/LikesHobbyRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/LikesHobbyRelationship.java index 28d8b1656..9cff26496 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/LikesHobbyRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/LikesHobbyRelationship.java @@ -29,11 +29,11 @@ import org.springframework.data.neo4j.types.CartesianPoint2d; @RelationshipProperties public class LikesHobbyRelationship { + private final Integer since; + @RelationshipId private Long id; - private final Integer since; - private Boolean active; // use some properties that require conversion @@ -69,6 +69,18 @@ public class LikesHobbyRelationship { this.point = point; } + public Hobby getHobby() { + return this.hobby; + } + + public void setHobby(Hobby hobby) { + this.hobby = hobby; + } + + public Integer getSince() { + return this.since; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -78,31 +90,23 @@ public class LikesHobbyRelationship { return false; } LikesHobbyRelationship that = (LikesHobbyRelationship) o; - return since.equals(that.since) && Objects.equals(active, that.active) && Objects.equals(localDate, that.localDate) - && myEnum == that.myEnum && Objects.equals(point, that.point); + return this.since.equals(that.since) && Objects.equals(this.active, that.active) + && Objects.equals(this.localDate, that.localDate) && this.myEnum == that.myEnum + && Objects.equals(this.point, that.point); } @Override public int hashCode() { - return Objects.hash(since, active, localDate, myEnum, point); - } - - public Hobby getHobby() { - return hobby; - } - - public void setHobby(Hobby hobby) { - this.hobby = hobby; - } - - public Integer getSince() { - return since; + return Objects.hash(this.since, this.active, this.localDate, this.myEnum, this.point); } /** * The missing javadoc */ public enum MyEnum { + SOMETHING, SOMETHING_DIFFERENT + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Metric.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Metric.java index cf728c640..bfbacef3a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Metric.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Metric.java @@ -29,20 +29,12 @@ import org.springframework.data.neo4j.core.schema.Node; @Node("Metric") public abstract class Metric { - @Id - @GeneratedValue - Long id; - @DynamicLabels public List dynamicLabels = new ArrayList<>(); - public Long getId() { - return id; - } - - public List getDynamicLabels() { - return dynamicLabels; - } + @Id + @GeneratedValue + Long id; private String name; @@ -50,11 +42,20 @@ public abstract class Metric { this.name = name; } + public Long getId() { + return this.id; + } + + public List getDynamicLabels() { + return this.dynamicLabels; + } + public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Multiple1O1Relationships.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Multiple1O1Relationships.java index f688aa474..65816e2a2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Multiple1O1Relationships.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Multiple1O1Relationships.java @@ -22,7 +22,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; /** * @author Michael J. Simons - * @soundtrack Dream Theater - Scenes From A Memory */ @Node public class Multiple1O1Relationships { @@ -40,7 +39,7 @@ public class Multiple1O1Relationships { private AltPerson person2; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -48,7 +47,7 @@ public class Multiple1O1Relationships { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -56,7 +55,7 @@ public class Multiple1O1Relationships { } public AltPerson getPerson1() { - return person1; + return this.person1; } public void setPerson1(AltPerson person1) { @@ -64,10 +63,11 @@ public class Multiple1O1Relationships { } public AltPerson getPerson2() { - return person2; + return this.person2; } public void setPerson2(AltPerson person2) { this.person2 = person2; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleLabels.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleLabels.java index 03e7e2efe..dbb0ff6c3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleLabels.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleLabels.java @@ -28,29 +28,34 @@ public class MultipleLabels { /** * An entity */ - @Node({"A", "B", "C"}) + @Node({ "A", "B", "C" }) public static class MultipleLabelsEntity { + + @Relationship(type = "HAS") + public MultipleLabelsEntity otherMultipleLabelEntity; + @Id @GeneratedValue private Long id; - @Relationship(type = "HAS") - public MultipleLabelsEntity otherMultipleLabelEntity; } /** * An entity */ - @Node({"X", "Y", "Z"}) + @Node({ "X", "Y", "Z" }) public static class MultipleLabelsEntityWithAssignedId { + @Id public Long id; + @Relationship(type = "HAS") + public MultipleLabelsEntityWithAssignedId otherMultipleLabelEntity; + public MultipleLabelsEntityWithAssignedId(Long id) { this.id = id; } - @Relationship(type = "HAS") - public MultipleLabelsEntityWithAssignedId otherMultipleLabelEntity; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleRelationshipsThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleRelationshipsThing.java index 5cf86209b..18b5aa35e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleRelationshipsThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MultipleRelationshipsThing.java @@ -22,8 +22,9 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; /** - * This thing has several relationships to other things of the same kind but with a different type. It is used to test - * whether all types are stored correctly even if those relationships point to the same instance of the thing. + * This thing has several relationships to other things of the same kind but with a + * different type. It is used to test whether all types are stored correctly even if those + * relationships point to the same instance of the thing. * * @author Michael J. Simons */ @@ -47,15 +48,15 @@ public class MultipleRelationshipsThing { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public MultipleRelationshipsThing getTypeA() { - return typeA; + return this.typeA; } public void setTypeA(MultipleRelationshipsThing typeA) { @@ -63,7 +64,7 @@ public class MultipleRelationshipsThing { } public List getTypeB() { - return typeB; + return this.typeB; } public void setTypeB(List typeB) { @@ -71,10 +72,11 @@ public class MultipleRelationshipsThing { } public List getTypeC() { - return typeC; + return this.typeC; } public void setTypeC(List typeC) { this.typeC = typeC; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableChild.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableChild.java index 5175dedcf..da4b1e506 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableChild.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableChild.java @@ -30,10 +30,11 @@ public class MutableChild { private Long id; public Long getId() { - return id; + return this.id; } public void setId(Long id) { this.id = id; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableParent.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableParent.java index d50559639..ebb3cb185 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableParent.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/MutableParent.java @@ -34,7 +34,7 @@ public class MutableParent { private List children; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -42,10 +42,11 @@ public class MutableParent { } public List getChildren() { - return children; + return this.children; } public void setChildren(List children) { this.children = children; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnly.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnly.java index 90a4b4a74..2bc7a323d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnly.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnly.java @@ -28,4 +28,5 @@ public interface NamesOnly { @Value("#{target.firstName + ' ' + target.lastName}") String getFullName(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnlyDto.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnlyDto.java index 2cbf35ba9..9281e2ca6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnlyDto.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesOnlyDto.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.integration.shared.common; public class NamesOnlyDto { private final String firstName; + private final String lastName; public NamesOnlyDto(String firstName, String lastName) { @@ -29,10 +30,11 @@ public class NamesOnlyDto { } public String getFirstName() { - return firstName; + return this.firstName; } public String getLastName() { - return lastName; + return this.lastName; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesWithSpELCity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesWithSpELCity.java index 6417bbbac..1a37f14cd 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesWithSpELCity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/NamesWithSpELCity.java @@ -28,4 +28,5 @@ public interface NamesWithSpELCity { @Value("#{target.address.city}") String getCity(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OffsetTemporalEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OffsetTemporalEntity.java index 051e7a6a2..01da6b147 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OffsetTemporalEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OffsetTemporalEntity.java @@ -45,11 +45,11 @@ public class OffsetTemporalEntity { } public UUID getUuid() { - return uuid; + return this.uuid; } public OffsetDateTime getProperty1() { - return property1; + return this.property1; } public void setProperty1(OffsetDateTime property1) { @@ -57,10 +57,11 @@ public class OffsetTemporalEntity { } public LocalTime getProperty2() { - return property2; + return this.property2; } public void setProperty2(LocalTime property2) { this.property2 = property2; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneSource.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneSource.java index fb1f47e5c..d2cad8bed 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneSource.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneSource.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -40,18 +42,23 @@ public class OneToOneSource { return this.name; } - public OneToOneTarget getTarget() { - return this.target; - } - public void setName(String name) { this.name = name; } + public OneToOneTarget getTarget() { + return this.target; + } + public void setTarget(OneToOneTarget target) { this.target = target; } + protected boolean canEqual(final Object other) { + return other instanceof OneToOneSource; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -65,31 +72,26 @@ public class OneToOneSource { } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$target = this.getTarget(); final Object other$target = other.getTarget(); - if (this$target == null ? other$target != null : !this$target.equals(other$target)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof OneToOneSource; + return Objects.equals(this$target, other$target); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); final Object $target = this.getTarget(); - result = result * PRIME + ($target == null ? 43 : $target.hashCode()); + result = (result * PRIME) + (($target != null) ? $target.hashCode() : 43); return result; } + @Override public String toString() { return "OneToOneSource(name=" + this.getName() + ", target=" + this.getTarget() + ")"; } @@ -98,7 +100,9 @@ public class OneToOneSource { * Simple DTO projection for OneToOneSource */ public static class OneToOneSourceProjection { + String name; + OneToOneTarget target; public OneToOneSourceProjection() { @@ -108,18 +112,23 @@ public class OneToOneSource { return this.name; } - public OneToOneTarget getTarget() { - return this.target; - } - public void setName(String name) { this.name = name; } + public OneToOneTarget getTarget() { + return this.target; + } + public void setTarget(OneToOneTarget target) { this.target = target; } + protected boolean canEqual(final Object other) { + return other instanceof OneToOneSourceProjection; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -133,33 +142,31 @@ public class OneToOneSource { } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$target = this.getTarget(); final Object other$target = other.getTarget(); - if (this$target == null ? other$target != null : !this$target.equals(other$target)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof OneToOneSourceProjection; + return Objects.equals(this$target, other$target); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); final Object $target = this.getTarget(); - result = result * PRIME + ($target == null ? 43 : $target.hashCode()); + result = (result * PRIME) + (($target != null) ? $target.hashCode() : 43); return result; } + @Override public String toString() { - return "OneToOneSource.OneToOneSourceProjection(name=" + this.getName() + ", target=" + this.getTarget() + ")"; + return "OneToOneSource.OneToOneSourceProjection(name=" + this.getName() + ", target=" + this.getTarget() + + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneTarget.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneTarget.java index b5a048dd1..90ad93cc6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneTarget.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/OneToOneTarget.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -40,6 +42,11 @@ public class OneToOneTarget { this.name = name; } + protected boolean canEqual(final Object other) { + return other instanceof OneToOneTarget; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -53,25 +60,21 @@ public class OneToOneTarget { } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof OneToOneTarget; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); return result; } + @Override public String toString() { return "OneToOneTarget(name=" + this.getName() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ParentNode.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ParentNode.java index 39c3b6c45..1f4bd5aca 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ParentNode.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ParentNode.java @@ -21,10 +21,10 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Zurück zum Glück */ @Node public class ParentNode { + @Id @GeneratedValue private Long id; @@ -32,14 +32,15 @@ public class ParentNode { private String someAttribute; public Long getId() { - return id; + return this.id; } public String getSomeAttribute() { - return someAttribute; + return this.someAttribute; } public void setSomeAttribute(String someAttribute) { this.someAttribute = someAttribute; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Person.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Person.java index 4e22e1a2c..182b39508 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Person.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Person.java @@ -29,7 +29,9 @@ public class Person { @Id @GeneratedValue private Long id; + private String firstName; + private String lastName; private int primitiveValue; // never used but always null @@ -37,30 +39,66 @@ public class Person { @Relationship("LIVES_AT") private Address address; + public Long getId() { + return this.id; + } + + // The getters are needed for Spring Expression Language in `NamesOnly` + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return this.lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Address getAddress() { + return this.address; + } + + @Override + public String toString() { + return "Person{" + "id=" + this.id + ", firstName='" + this.firstName + '\'' + ", lastName='" + this.lastName + + '\'' + ", address=" + this.address + '}'; + } + /** * Address of a person. */ @Node public static class Address { - @Id @GeneratedValue + + @Id + @GeneratedValue private Long id; + private String zipCode; + private String city; + private String street; @Relationship("BASED_IN") private Country country; public Long getId() { - return id; + return this.id; } public String getZipCode() { - return zipCode; + return this.zipCode; } public String getCity() { - return city; + return this.city; } public void setCity(String city) { @@ -68,7 +106,7 @@ public class Person { } public String getStreet() { - return street; + return this.street; } public void setStreet(String street) { @@ -76,7 +114,7 @@ public class Person { } public Country getCountry() { - return country; + return this.country; } public void setCountry(Country country) { @@ -88,14 +126,17 @@ public class Person { */ @Node("YetAnotherCountryEntity") public static class Country { + @Id @GeneratedValue private Long id; + private String name; + private String countryCode; public String getCountryCode() { - return countryCode; + return this.countryCode; } public void setCountryCode(String countryCode) { @@ -103,43 +144,15 @@ public class Person { } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } + } - public Long getId() { - return id; - } - - // The getters are needed for Spring Expression Language in `NamesOnly` - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - public Address getAddress() { - return address; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - @Override - public String toString() { - return "Person{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", address=" - + address + '}'; - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonDepartmentQueryResult.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonDepartmentQueryResult.java index 90c097c63..b9eda9825 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonDepartmentQueryResult.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonDepartmentQueryResult.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.integration.shared.common; public class PersonDepartmentQueryResult { private final PersonEntity person; + private final DepartmentEntity department; public PersonDepartmentQueryResult(PersonEntity person, DepartmentEntity department) { @@ -35,4 +36,5 @@ public class PersonDepartmentQueryResult { public DepartmentEntity getDepartment() { return this.department; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonEntity.java index dc48e795c..5b7d37eeb 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonEntity.java @@ -23,8 +23,10 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class PersonEntity { + @Id private final String id; + private final String email; public PersonEntity(String id, String email) { @@ -39,4 +41,5 @@ public class PersonEntity { public String getEmail() { return this.email; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonProjection.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonProjection.java index e314d912f..6f08e905c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonProjection.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonProjection.java @@ -26,4 +26,5 @@ public interface PersonProjection { String getFirstName(); String getSameValue(); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonSummary.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonSummary.java index ec98b60d6..8394c6c4c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonSummary.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonSummary.java @@ -30,6 +30,9 @@ public interface PersonSummary { * nested projection */ interface AddressSummary { + String getCity(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAllConstructor.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAllConstructor.java index d95a5db4b..fe5fbacd1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAllConstructor.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAllConstructor.java @@ -15,16 +15,18 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + import org.neo4j.driver.types.Point; + 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 java.time.Instant; -import java.time.LocalDate; -import java.util.List; - /** * @author Gerrit Meier * @author Michael J. Simons @@ -39,9 +41,6 @@ public class PersonWithAllConstructor { private final String name; - @Property("first_name") - private String firstName; - private final String sameValue; private final Boolean cool; @@ -50,15 +49,19 @@ public class PersonWithAllConstructor { private final LocalDate bornOn; - private String nullable; - - private List things; - private final Point place; private final Instant createdAt; - public PersonWithAllConstructor(Long id, String name, String firstName, String sameValue, Boolean cool, Long personNumber, LocalDate bornOn, String nullable, List things, Point place, Instant createdAt) { + @Property("first_name") + private String firstName; + + private String nullable; + + private List things; + + public PersonWithAllConstructor(Long id, String name, String firstName, String sameValue, Boolean cool, + Long personNumber, LocalDate bornOn, String nullable, List things, Point place, Instant createdAt) { this.id = id; this.name = name; this.firstName = firstName; @@ -84,6 +87,10 @@ public class PersonWithAllConstructor { return this.firstName; } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getSameValue() { return this.sameValue; } @@ -104,10 +111,18 @@ public class PersonWithAllConstructor { return this.nullable; } + public void setNullable(String nullable) { + this.nullable = nullable; + } + public List getThings() { return this.things; } + public void setThings(List things) { + this.things = things; + } + public Point getPlace() { return this.place; } @@ -116,18 +131,17 @@ public class PersonWithAllConstructor { return this.createdAt; } - public void setFirstName(String firstName) { - this.firstName = firstName; + protected boolean canEqual(final Object other) { + return other instanceof PersonWithAllConstructor; } - public void setNullable(String nullable) { - this.nullable = nullable; - } - - public void setThings(List things) { - this.things = things; + public PersonWithAllConstructor withId(Long id) { + return Objects.equals(this.id, id) ? this + : new PersonWithAllConstructor(id, this.name, this.firstName, this.sameValue, this.cool, + this.personNumber, this.bornOn, this.nullable, this.things, this.place, this.createdAt); } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -136,104 +150,100 @@ public class PersonWithAllConstructor { return false; } final PersonWithAllConstructor other = (PersonWithAllConstructor) o; - if (!other.canEqual((Object) this)) { + if (!other.canEqual(this)) { return false; } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { + if (!Objects.equals(this$name, other$name)) { return false; } final Object this$firstName = this.getFirstName(); final Object other$firstName = other.getFirstName(); - if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) { + if (!Objects.equals(this$firstName, other$firstName)) { return false; } final Object this$sameValue = this.getSameValue(); final Object other$sameValue = other.getSameValue(); - if (this$sameValue == null ? other$sameValue != null : !this$sameValue.equals(other$sameValue)) { + if (!Objects.equals(this$sameValue, other$sameValue)) { return false; } final Object this$cool = this.getCool(); final Object other$cool = other.getCool(); - if (this$cool == null ? other$cool != null : !this$cool.equals(other$cool)) { + if (!Objects.equals(this$cool, other$cool)) { return false; } final Object this$personNumber = this.getPersonNumber(); final Object other$personNumber = other.getPersonNumber(); - if (this$personNumber == null ? other$personNumber != null : !this$personNumber.equals(other$personNumber)) { + if (!Objects.equals(this$personNumber, other$personNumber)) { return false; } final Object this$bornOn = this.getBornOn(); final Object other$bornOn = other.getBornOn(); - if (this$bornOn == null ? other$bornOn != null : !this$bornOn.equals(other$bornOn)) { + if (!Objects.equals(this$bornOn, other$bornOn)) { return false; } final Object this$nullable = this.getNullable(); final Object other$nullable = other.getNullable(); - if (this$nullable == null ? other$nullable != null : !this$nullable.equals(other$nullable)) { + if (!Objects.equals(this$nullable, other$nullable)) { return false; } final Object this$things = this.getThings(); final Object other$things = other.getThings(); - if (this$things == null ? other$things != null : !this$things.equals(other$things)) { + if (!Objects.equals(this$things, other$things)) { return false; } final Object this$place = this.getPlace(); final Object other$place = other.getPlace(); - if (this$place == null ? other$place != null : !this$place.equals(other$place)) { + if (!Objects.equals(this$place, other$place)) { return false; } final Object this$createdAt = this.getCreatedAt(); final Object other$createdAt = other.getCreatedAt(); - if (this$createdAt == null ? other$createdAt != null : !this$createdAt.equals(other$createdAt)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof PersonWithAllConstructor; + return Objects.equals(this$createdAt, other$createdAt); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = result * PRIME + (($id != null) ? $id.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = result * PRIME + (($name != null) ? $name.hashCode() : 43); final Object $firstName = this.getFirstName(); - result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode()); + result = result * PRIME + (($firstName != null) ? $firstName.hashCode() : 43); final Object $sameValue = this.getSameValue(); - result = result * PRIME + ($sameValue == null ? 43 : $sameValue.hashCode()); + result = result * PRIME + (($sameValue != null) ? $sameValue.hashCode() : 43); final Object $cool = this.getCool(); - result = result * PRIME + ($cool == null ? 43 : $cool.hashCode()); + result = result * PRIME + (($cool != null) ? $cool.hashCode() : 43); final Object $personNumber = this.getPersonNumber(); - result = result * PRIME + ($personNumber == null ? 43 : $personNumber.hashCode()); + result = result * PRIME + (($personNumber != null) ? $personNumber.hashCode() : 43); final Object $bornOn = this.getBornOn(); - result = result * PRIME + ($bornOn == null ? 43 : $bornOn.hashCode()); + result = result * PRIME + (($bornOn != null) ? $bornOn.hashCode() : 43); final Object $nullable = this.getNullable(); - result = result * PRIME + ($nullable == null ? 43 : $nullable.hashCode()); + result = result * PRIME + (($nullable != null) ? $nullable.hashCode() : 43); final Object $things = this.getThings(); - result = result * PRIME + ($things == null ? 43 : $things.hashCode()); + result = result * PRIME + (($things != null) ? $things.hashCode() : 43); final Object $place = this.getPlace(); - result = result * PRIME + ($place == null ? 43 : $place.hashCode()); + result = result * PRIME + (($place != null) ? $place.hashCode() : 43); final Object $createdAt = this.getCreatedAt(); - result = result * PRIME + ($createdAt == null ? 43 : $createdAt.hashCode()); + result = result * PRIME + (($createdAt != null) ? $createdAt.hashCode() : 43); return result; } + @Override public String toString() { - return "PersonWithAllConstructor(id=" + this.getId() + ", name=" + this.getName() + ", firstName=" + this.getFirstName() + ", sameValue=" + this.getSameValue() + ", cool=" + this.getCool() + ", personNumber=" + this.getPersonNumber() + ", bornOn=" + this.getBornOn() + ", nullable=" + this.getNullable() + ", things=" + this.getThings() + ", place=" + this.getPlace() + ", createdAt=" + this.getCreatedAt() + ")"; + return "PersonWithAllConstructor(id=" + this.getId() + ", name=" + this.getName() + ", firstName=" + + this.getFirstName() + ", sameValue=" + this.getSameValue() + ", cool=" + this.getCool() + + ", personNumber=" + this.getPersonNumber() + ", bornOn=" + this.getBornOn() + ", nullable=" + + this.getNullable() + ", things=" + this.getThings() + ", place=" + this.getPlace() + ", createdAt=" + + this.getCreatedAt() + ")"; } - public PersonWithAllConstructor withId(Long id) { - return this.id == id ? this : new PersonWithAllConstructor(id, this.name, this.firstName, this.sameValue, this.cool, this.personNumber, this.bornOn, this.nullable, this.things, this.place, this.createdAt); - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAssignedId.java index 28bdc82a0..e464306b4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithAssignedId.java @@ -32,7 +32,7 @@ public class PersonWithAssignedId { private String lastName; public String getId() { - return id; + return this.id; } public void setId(String id) { @@ -40,7 +40,7 @@ public class PersonWithAssignedId { } public String getFirstName() { - return firstName; + return this.firstName; } public void setFirstName(String firstName) { @@ -48,10 +48,11 @@ public class PersonWithAssignedId { } public String getLastName() { - return lastName; + return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithNoConstructor.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithNoConstructor.java index a11370116..a748230f9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithNoConstructor.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithNoConstructor.java @@ -42,35 +42,38 @@ public class PersonWithNoConstructor { return this.id; } - public String getName() { - return this.name; - } - - public String getFirstName() { - return this.firstName; - } - - public String getMiddleName() { - return this.middleName; - } - public void setId(Long id) { this.id = id; } + public String getName() { + return this.name; + } + public void setName(String name) { this.name = name; } + public String getFirstName() { + return this.firstName; + } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getMiddleName() { + return this.middleName; + } + public void setMiddleName(String middleName) { this.middleName = middleName; } + @Override public String toString() { - return "PersonWithNoConstructor(id=" + this.getId() + ", name=" + this.getName() + ", firstName=" + this.getFirstName() + ", middleName=" + this.getMiddleName() + ")"; + return "PersonWithNoConstructor(id=" + this.getId() + ", name=" + this.getName() + ", firstName=" + + this.getFirstName() + ", middleName=" + this.getMiddleName() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationship.java index 712cc36c7..a838beae2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationship.java @@ -44,7 +44,7 @@ public class PersonWithRelationship { private Club club; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -52,7 +52,7 @@ public class PersonWithRelationship { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -60,7 +60,7 @@ public class PersonWithRelationship { } public Hobby getHobbies() { - return hobbies; + return this.hobbies; } public void setHobbies(Hobby hobbies) { @@ -68,7 +68,7 @@ public class PersonWithRelationship { } public Club getClub() { - return club; + return this.club; } public void setClub(Club club) { @@ -76,7 +76,7 @@ public class PersonWithRelationship { } public List getPets() { - return pets; + return this.pets; } public void setPets(List pets) { @@ -87,8 +87,11 @@ public class PersonWithRelationship { * Simple person with hobbies relationship to enforce non-cyclic querying. */ public interface PersonWithHobby { + String getName(); Hobby getHobbies(); + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties.java index ae4c1f74c..7a57ebf1c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties.java @@ -32,10 +32,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class PersonWithRelationshipWithProperties { - @Id - @GeneratedValue - private Long id; - private final String name; @Relationship("LIKES") @@ -44,6 +40,10 @@ public class PersonWithRelationshipWithProperties { @Relationship("WORKS_IN") private final WorksInClubRelationship club; + @Id + @GeneratedValue + private Long id; + @Relationship("OWNS") private Set pets; @@ -51,14 +51,16 @@ public class PersonWithRelationshipWithProperties { private List clubs; @PersistenceCreator - public PersonWithRelationshipWithProperties(Long id, String name, List hobbies, WorksInClubRelationship club) { + public PersonWithRelationshipWithProperties(Long id, String name, List hobbies, + WorksInClubRelationship club) { this.id = id; this.name = name; this.hobbies = hobbies; this.club = club; } - public PersonWithRelationshipWithProperties(String name, List hobbies, WorksInClubRelationship club) { + public PersonWithRelationshipWithProperties(String name, List hobbies, + WorksInClubRelationship club) { this.name = name; this.hobbies = hobbies; this.club = club; @@ -69,26 +71,27 @@ public class PersonWithRelationshipWithProperties { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public List getHobbies() { - return hobbies; + return this.hobbies; } public WorksInClubRelationship getClub() { - return club; + return this.club; } public Set getPets() { - return pets; + return this.pets; } public List getClubs() { - return clubs; + return this.clubs; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties2.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties2.java index 255b755f5..135534c86 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelationshipWithProperties2.java @@ -28,12 +28,12 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class PersonWithRelationshipWithProperties2 { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - @Relationship("LIKES") private Set hobbies; @@ -42,14 +42,15 @@ public class PersonWithRelationshipWithProperties2 { } public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public Set getHobbies() { - return hobbies; + return this.hobbies; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelatives.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelatives.java index 2866fd8ad..145a55f60 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelatives.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithRelatives.java @@ -29,40 +29,12 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class PersonWithRelatives { - /** - * Some enum representing relatives. - */ - public enum TypeOfRelative { - HAS_WIFE, HAS_DAUGHTER, HAS_SON, RELATIVE_1, RELATIVE_2 - } - - /** - * Some enum representing pets. - */ - public enum TypeOfPet { - CATS, DOGS, FISH, MONSTERS - } - - /** - * Some enum representing hobby states. - */ - public enum TypeOfHobby { - ACTIVE, WATCHING - } - - /** - * Some enum representing sport genres. - */ - public enum TypeOfClub { - FOOTBALL, BASEBALL - } + private final String name; @Id @GeneratedValue private Long id; - private final String name; - private Map relatives = new HashMap<>(); private Map> pets = new HashMap<>(); @@ -76,26 +48,63 @@ public class PersonWithRelatives { } public long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public Map getRelatives() { - return relatives; + return this.relatives; } public Map> getPets() { - return pets; + return this.pets; } public Map> getHobbies() { - return hobbies; + return this.hobbies; } public Map getClubs() { - return clubs; + return this.clubs; } + + /** + * Some enum representing relatives. + */ + public enum TypeOfRelative { + + HAS_WIFE, HAS_DAUGHTER, HAS_SON, RELATIVE_1, RELATIVE_2 + + } + + /** + * Some enum representing pets. + */ + public enum TypeOfPet { + + CATS, DOGS, FISH, MONSTERS + + } + + /** + * Some enum representing hobby states. + */ + public enum TypeOfHobby { + + ACTIVE, WATCHING + + } + + /** + * Some enum representing sport genres. + */ + public enum TypeOfClub { + + FOOTBALL, BASEBALL + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithStringlyTypedRelatives.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithStringlyTypedRelatives.java index 33c78007b..fd925abbe 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithStringlyTypedRelatives.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithStringlyTypedRelatives.java @@ -29,12 +29,12 @@ import org.springframework.data.neo4j.core.schema.Node; @Node public class PersonWithStringlyTypedRelatives { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - private Map relatives = new HashMap<>(); private Map> pets = new HashMap<>(); @@ -48,23 +48,23 @@ public class PersonWithStringlyTypedRelatives { } public long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public Map getRelatives() { - return relatives; + return this.relatives; } public Map> getPets() { - return pets; + return this.pets; } public Map> getHobbies() { - return hobbies; + return this.hobbies; } public void setHobbies(Map> hobbies) { @@ -72,10 +72,11 @@ public class PersonWithStringlyTypedRelatives { } public Map getClubs() { - return clubs; + return this.clubs; } public void setClubs(Map clubs) { this.clubs = clubs; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithWither.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithWither.java index 219753628..1c920cf5f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithWither.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/PersonWithWither.java @@ -24,7 +24,7 @@ import org.springframework.data.neo4j.core.schema.Node; * "https://docs.spring.io/spring-data/commons/docs/current/reference/html/#mapping.property-population">here. */ @Node -public class PersonWithWither { +public final class PersonWithWither { @Id @GeneratedValue @@ -53,7 +53,9 @@ public class PersonWithWither { return this.name; } + @Override public String toString() { return "PersonWithWither(id=" + this.getId() + ", name=" + this.getName() + ")"; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Pet.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Pet.java index 0c0cc1cab..03771ee11 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Pet.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Pet.java @@ -15,32 +15,28 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.List; +import java.util.Objects; +import java.util.Set; + import org.springframework.data.annotation.PersistenceCreator; 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.Relationship; -import java.util.List; -import java.util.Set; - /** * @author Gerrit Meier */ @Node public class Pet { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - - public Pet(long id, String name) { - this(name); - this.id = id; - } - @Relationship("Has") private Set hobbies; @@ -53,13 +49,18 @@ public class Pet { @Relationship("Has") private List things; + public Pet(long id, String name) { + this(name); + this.id = id; + } + @PersistenceCreator public Pet(String name) { this.name = name; } public Set getHobbies() { - return hobbies; + return this.hobbies; } public void setHobbies(Set hobbies) { @@ -67,7 +68,7 @@ public class Pet { } public List getFriends() { - return friends; + return this.friends; } public void setFriends(List friends) { @@ -75,7 +76,7 @@ public class Pet { } public Long getId() { - return id; + return this.id; } public String getName() { @@ -90,6 +91,11 @@ public class Pet { return this.things; } + protected boolean canEqual(final Object other) { + return other instanceof Pet; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -103,28 +109,23 @@ public class Pet { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof Pet; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); return result; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Port.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Port.java index 9d1e7b7b2..29a491a8f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/Port.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/Port.java @@ -28,10 +28,14 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node("Port") public class Port { + @Id @GeneratedValue private UUID id; + private String code; + @DynamicLabels private List labels; + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTest1O1.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTest1O1.java index 5849b83e9..aeba705d3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTest1O1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTest1O1.java @@ -22,4 +22,5 @@ import org.springframework.data.neo4j.core.schema.Node; */ @Node public class ProjectionTest1O1 extends ProjectionTestBase { + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestBase.java index 0b15435eb..12a303ccc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestBase.java @@ -30,14 +30,15 @@ public abstract class ProjectionTestBase { private String name; public Long getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public void setName(String name) { this.name = name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel1.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel1.java index f7ecd05eb..95e111da6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel1.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel1.java @@ -29,6 +29,7 @@ public class ProjectionTestLevel1 extends ProjectionTestBase { private List level2 = new ArrayList<>(); public List getLevel2() { - return level2; + return this.level2; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel2.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel2.java index e19b6dfc2..07b226a66 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestLevel2.java @@ -26,10 +26,11 @@ public class ProjectionTestLevel2 extends ProjectionTestBase { private ProjectionTestRoot backToRoot; public ProjectionTestRoot getBackToRoot() { - return backToRoot; + return this.backToRoot; } public void setBackToRoot(ProjectionTestRoot backToRoot) { this.backToRoot = backToRoot; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestRoot.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestRoot.java index 497e1eaff..6468a8eda 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestRoot.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ProjectionTestRoot.java @@ -31,14 +31,15 @@ public class ProjectionTestRoot extends ProjectionTestBase { private List level1 = new ArrayList<>(); public List getLevel1() { - return level1; + return this.level1; } public ProjectionTest1O1 getOneOone() { - return oneOone; + return this.oneOone; } public void setOneOone(ProjectionTest1O1 oneOone) { this.oneOone = oneOone; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsAsConstructorParametersEntities.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsAsConstructorParametersEntities.java index af4e3ed08..c5df13bb1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsAsConstructorParametersEntities.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsAsConstructorParametersEntities.java @@ -25,56 +25,60 @@ import org.springframework.data.neo4j.core.schema.Relationship; */ public final class RelationshipsAsConstructorParametersEntities { + private RelationshipsAsConstructorParametersEntities() { + } + /** * Parent or master node. */ @Node public static class NodeTypeA { + private final String name; + @Id @GeneratedValue private Long id; - private final String name; - public NodeTypeA(String name) { this.name = name; } public String getName() { - return name; + return this.name; } + } /** - * Child node having two immutable fields assigned via ctor and a generated id that is assigend from SDN. + * Child node having two immutable fields assigned via ctor and a generated id that is + * assigend from SDN. */ @Node public static class NodeTypeB { - @Id - @GeneratedValue - private Long id; - @Relationship("BELONGS_TO") private final NodeTypeA nodeTypeA; private final String name; + @Id + @GeneratedValue + private Long id; + public NodeTypeB(NodeTypeA nodeTypeA, String name) { this.nodeTypeA = nodeTypeA; this.name = name; } public NodeTypeA getNodeTypeA() { - return nodeTypeA; + return this.nodeTypeA; } public String getName() { - return name; + return this.name; } + } - private RelationshipsAsConstructorParametersEntities() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsITBase.java index 1a6629559..ac6cda6aa 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/RelationshipsITBase.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; @@ -32,6 +33,7 @@ public abstract class RelationshipsITBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; protected final Driver driver; + private final BookmarkCapture bookmarkCapture; protected RelationshipsITBase(Driver driver, BookmarkCapture bookmarkCapture) { @@ -41,10 +43,12 @@ public abstract class RelationshipsITBase { @BeforeEach void setup() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { transaction.run("MATCH (n) detach delete n").consume(); transaction.commit(); - bookmarkCapture.seedWith(session.lastBookmarks()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SameIdProperty.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SameIdProperty.java index ae0b0f917..d668313f7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SameIdProperty.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SameIdProperty.java @@ -15,6 +15,10 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; @@ -22,19 +26,18 @@ import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; -import java.util.ArrayList; -import java.util.List; - /** * @author Gerrit Meier */ @SuppressWarnings("HiddenField") public class SameIdProperty { + /** * @author Gerrit Meier */ @Node("Pod") public static class PodEntity { + @Id private String code; @@ -53,44 +56,44 @@ public class SameIdProperty { this.code = code; } - public boolean equals(final Object o) { - if (o == this) { - return true; - } - if (!(o instanceof PodEntity)) { - return false; - } - final PodEntity other = (PodEntity) o; - if (!other.canEqual((Object) this)) { - return false; - } - final Object this$code = this.getCode(); - final Object other$code = other.getCode(); - if (this$code == null ? other$code != null : !this$code.equals(other$code)) { - return false; - } - return true; - } - protected boolean canEqual(final Object other) { return other instanceof PodEntity; } + public PodEntity withCode(String code) { + return Objects.equals(this.code, code) ? this : new PodEntity(code); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof PodEntity other)) { + return false; + } + if (!other.canEqual(this)) { + return false; + } + final Object this$code = this.getCode(); + final Object other$code = other.getCode(); + return Objects.equals(this$code, other$code); + } + + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $code = this.getCode(); - result = result * PRIME + ($code == null ? 43 : $code.hashCode()); + result = result * PRIME + (($code != null) ? $code.hashCode() : 43); return result; } + @Override public String toString() { return "SameIdProperty.PodEntity(code=" + this.getCode() + ")"; } - public PodEntity withCode(String code) { - return this.code == code ? this : new PodEntity(code); - } } /** @@ -98,6 +101,7 @@ public class SameIdProperty { */ @Node("Pol") public static class PolEntity { + @Id private String code; @@ -116,18 +120,31 @@ public class SameIdProperty { return this.code; } - public List getRoutes() { - return this.routes; - } - public void setCode(String code) { this.code = code; } + public List getRoutes() { + return this.routes; + } + public void setRoutes(List routes) { this.routes = routes; } + protected boolean canEqual(final Object other) { + return other instanceof PolEntity; + } + + public PolEntity withCode(String code) { + return (this.code != code) ? new PolEntity(code, this.routes) : this; + } + + public PolEntity withRoutes(List routes) { + return Objects.equals(this.routes, routes) ? this : new PolEntity(this.code, routes); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -141,42 +158,30 @@ public class SameIdProperty { } final Object this$code = this.getCode(); final Object other$code = other.getCode(); - if (this$code == null ? other$code != null : !this$code.equals(other$code)) { + if (!Objects.equals(this$code, other$code)) { return false; } final Object this$routes = this.getRoutes(); final Object other$routes = other.getRoutes(); - if (this$routes == null ? other$routes != null : !this$routes.equals(other$routes)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof PolEntity; + return Objects.equals(this$routes, other$routes); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $code = this.getCode(); - result = result * PRIME + ($code == null ? 43 : $code.hashCode()); + result = result * PRIME + (($code != null) ? $code.hashCode() : 43); final Object $routes = this.getRoutes(); - result = result * PRIME + ($routes == null ? 43 : $routes.hashCode()); + result = result * PRIME + (($routes != null) ? $routes.hashCode() : 43); return result; } + @Override public String toString() { return "SameIdProperty.PolEntity(code=" + this.getCode() + ", routes=" + this.getRoutes() + ")"; } - public PolEntity withCode(String code) { - return this.code == code ? this : new PolEntity(code, this.routes); - } - - public PolEntity withRoutes(List routes) { - return this.routes == routes ? this : new PolEntity(this.code, routes); - } } /** @@ -184,6 +189,7 @@ public class SameIdProperty { */ @Node("PolWithRP") public static class PolEntityWithRelationshipProperties { + @Id private String code; @@ -202,18 +208,31 @@ public class SameIdProperty { return this.code; } - public List getRoutes() { - return this.routes; - } - public void setCode(String code) { this.code = code; } + public List getRoutes() { + return this.routes; + } + public void setRoutes(List routes) { this.routes = routes; } + protected boolean canEqual(final Object other) { + return other instanceof PolEntityWithRelationshipProperties; + } + + public PolEntityWithRelationshipProperties withCode(String code) { + return (this.code != code) ? new PolEntityWithRelationshipProperties(code, this.routes) : this; + } + + public PolEntityWithRelationshipProperties withRoutes(List routes) { + return (this.routes != routes) ? new PolEntityWithRelationshipProperties(this.code, routes) : this; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -227,42 +246,31 @@ public class SameIdProperty { } final Object this$code = this.getCode(); final Object other$code = other.getCode(); - if (this$code == null ? other$code != null : !this$code.equals(other$code)) { + if (!Objects.equals(this$code, other$code)) { return false; } final Object this$routes = this.getRoutes(); final Object other$routes = other.getRoutes(); - if (this$routes == null ? other$routes != null : !this$routes.equals(other$routes)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof PolEntityWithRelationshipProperties; + return Objects.equals(this$routes, other$routes); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $code = this.getCode(); - result = result * PRIME + ($code == null ? 43 : $code.hashCode()); + result = result * PRIME + (($code != null) ? $code.hashCode() : 43); final Object $routes = this.getRoutes(); - result = result * PRIME + ($routes == null ? 43 : $routes.hashCode()); + result = result * PRIME + (($routes != null) ? $routes.hashCode() : 43); return result; } + @Override public String toString() { - return "SameIdProperty.PolEntityWithRelationshipProperties(code=" + this.getCode() + ", routes=" + this.getRoutes() + ")"; + return "SameIdProperty.PolEntityWithRelationshipProperties(code=" + this.getCode() + ", routes=" + + this.getRoutes() + ")"; } - public PolEntityWithRelationshipProperties withCode(String code) { - return this.code == code ? this : new PolEntityWithRelationshipProperties(code, this.routes); - } - - public PolEntityWithRelationshipProperties withRoutes(List routes) { - return this.routes == routes ? this : new PolEntityWithRelationshipProperties(this.code, routes); - } } /** @@ -275,22 +283,26 @@ public class SameIdProperty { private Long id; private Double truck; + private String truckCurrency; private Double ft20; + private String ft20Currency; private Double ft40; + private String ft40Currency; private Double ft40HC; - private String ft40HCCurrency; + private String ft40HCCurrency; @TargetNode private PodEntity pod; - private RouteProperties(Long id, Double truck, String truckCurrency, Double ft20, String ft20Currency, Double ft40, String ft40Currency, Double ft40HC, String ft40HCCurrency, PodEntity pod) { + private RouteProperties(Long id, Double truck, String truckCurrency, Double ft20, String ft20Currency, + Double ft40, String ft40Currency, Double ft40HC, String ft40HCCurrency, PodEntity pod) { this.id = id; this.truck = truck; this.truckCurrency = truckCurrency; @@ -310,82 +322,143 @@ public class SameIdProperty { return this.id; } - public Double getTruck() { - return this.truck; - } - - public String getTruckCurrency() { - return this.truckCurrency; - } - - public Double getFt20() { - return this.ft20; - } - - public String getFt20Currency() { - return this.ft20Currency; - } - - public Double getFt40() { - return this.ft40; - } - - public String getFt40Currency() { - return this.ft40Currency; - } - - public Double getFt40HC() { - return this.ft40HC; - } - - public String getFt40HCCurrency() { - return this.ft40HCCurrency; - } - - public PodEntity getPod() { - return this.pod; - } - public void setId(Long id) { this.id = id; } + public Double getTruck() { + return this.truck; + } + public void setTruck(Double truck) { this.truck = truck; } + public String getTruckCurrency() { + return this.truckCurrency; + } + public void setTruckCurrency(String truckCurrency) { this.truckCurrency = truckCurrency; } + public Double getFt20() { + return this.ft20; + } + public void setFt20(Double ft20) { this.ft20 = ft20; } + public String getFt20Currency() { + return this.ft20Currency; + } + public void setFt20Currency(String ft20Currency) { this.ft20Currency = ft20Currency; } + public Double getFt40() { + return this.ft40; + } + public void setFt40(Double ft40) { this.ft40 = ft40; } + public String getFt40Currency() { + return this.ft40Currency; + } + public void setFt40Currency(String ft40Currency) { this.ft40Currency = ft40Currency; } + public Double getFt40HC() { + return this.ft40HC; + } + public void setFt40HC(Double ft40HC) { this.ft40HC = ft40HC; } + public String getFt40HCCurrency() { + return this.ft40HCCurrency; + } + public void setFt40HCCurrency(String ft40HCCurrency) { this.ft40HCCurrency = ft40HCCurrency; } + public PodEntity getPod() { + return this.pod; + } + public void setPod(PodEntity pod) { this.pod = pod; } + protected boolean canEqual(final Object other) { + return other instanceof RouteProperties; + } + + public RouteProperties withId(Long id) { + return (this.id != id) ? new RouteProperties(id, this.truck, this.truckCurrency, this.ft20, + this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) : this; + } + + public RouteProperties withTruck(Double truck) { + return (this.truck != truck) ? new RouteProperties(this.id, truck, this.truckCurrency, this.ft20, + this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) : this; + } + + public RouteProperties withTruckCurrency(String truckCurrency) { + return Objects.equals(this.truckCurrency, truckCurrency) ? this + : new RouteProperties(this.id, this.truck, truckCurrency, this.ft20, this.ft20Currency, this.ft40, + this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); + } + + public RouteProperties withFt20(Double ft20) { + return (this.ft20 != ft20) ? new RouteProperties(this.id, this.truck, this.truckCurrency, ft20, + this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) : this; + } + + public RouteProperties withFt20Currency(String ft20Currency) { + return (this.ft20Currency != ft20Currency) ? new RouteProperties(this.id, this.truck, this.truckCurrency, + this.ft20, ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) + : this; + } + + public RouteProperties withFt40(Double ft40) { + return (this.ft40 != ft40) ? new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, + this.ft20Currency, ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) : this; + } + + public RouteProperties withFt40Currency(String ft40Currency) { + return (this.ft40Currency != ft40Currency) ? new RouteProperties(this.id, this.truck, this.truckCurrency, + this.ft20, this.ft20Currency, this.ft40, ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod) + : this; + } + + public RouteProperties withFt40HC(Double ft40HC) { + return Objects.equals(this.ft40HC, ft40HC) ? this + : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, + this.ft40, this.ft40Currency, ft40HC, this.ft40HCCurrency, this.pod); + } + + public RouteProperties withFt40HCCurrency(String ft40HCCurrency) { + return (this.ft40HCCurrency != ft40HCCurrency) + ? new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, + this.ft40, this.ft40Currency, this.ft40HC, ft40HCCurrency, this.pod) + : this; + } + + public RouteProperties withPod(PodEntity pod) { + return (this.pod != pod) ? new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, + this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, pod) : this; + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -399,129 +472,90 @@ public class SameIdProperty { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$truck = this.getTruck(); final Object other$truck = other.getTruck(); - if (this$truck == null ? other$truck != null : !this$truck.equals(other$truck)) { + if (!Objects.equals(this$truck, other$truck)) { return false; } final Object this$truckCurrency = this.getTruckCurrency(); final Object other$truckCurrency = other.getTruckCurrency(); - if (this$truckCurrency == null ? other$truckCurrency != null : !this$truckCurrency.equals(other$truckCurrency)) { + if (!Objects.equals(this$truckCurrency, other$truckCurrency)) { return false; } final Object this$ft20 = this.getFt20(); final Object other$ft20 = other.getFt20(); - if (this$ft20 == null ? other$ft20 != null : !this$ft20.equals(other$ft20)) { + if (!Objects.equals(this$ft20, other$ft20)) { return false; } final Object this$ft20Currency = this.getFt20Currency(); final Object other$ft20Currency = other.getFt20Currency(); - if (this$ft20Currency == null ? other$ft20Currency != null : !this$ft20Currency.equals(other$ft20Currency)) { + if (!Objects.equals(this$ft20Currency, other$ft20Currency)) { return false; } final Object this$ft40 = this.getFt40(); final Object other$ft40 = other.getFt40(); - if (this$ft40 == null ? other$ft40 != null : !this$ft40.equals(other$ft40)) { + if (!Objects.equals(this$ft40, other$ft40)) { return false; } final Object this$ft40Currency = this.getFt40Currency(); final Object other$ft40Currency = other.getFt40Currency(); - if (this$ft40Currency == null ? other$ft40Currency != null : !this$ft40Currency.equals(other$ft40Currency)) { + if (!Objects.equals(this$ft40Currency, other$ft40Currency)) { return false; } final Object this$ft40HC = this.getFt40HC(); final Object other$ft40HC = other.getFt40HC(); - if (this$ft40HC == null ? other$ft40HC != null : !this$ft40HC.equals(other$ft40HC)) { + if (!Objects.equals(this$ft40HC, other$ft40HC)) { return false; } final Object this$ft40HCCurrency = this.getFt40HCCurrency(); final Object other$ft40HCCurrency = other.getFt40HCCurrency(); - if (this$ft40HCCurrency == null ? other$ft40HCCurrency != null : !this$ft40HCCurrency.equals(other$ft40HCCurrency)) { + if (!Objects.equals(this$ft40HCCurrency, other$ft40HCCurrency)) { return false; } final Object this$pod = this.getPod(); final Object other$pod = other.getPod(); - if (this$pod == null ? other$pod != null : !this$pod.equals(other$pod)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof RouteProperties; + return Objects.equals(this$pod, other$pod); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $truck = this.getTruck(); - result = result * PRIME + ($truck == null ? 43 : $truck.hashCode()); + result = (result * PRIME) + (($truck != null) ? $truck.hashCode() : 43); final Object $truckCurrency = this.getTruckCurrency(); - result = result * PRIME + ($truckCurrency == null ? 43 : $truckCurrency.hashCode()); + result = (result * PRIME) + (($truckCurrency != null) ? $truckCurrency.hashCode() : 43); final Object $ft20 = this.getFt20(); - result = result * PRIME + ($ft20 == null ? 43 : $ft20.hashCode()); + result = (result * PRIME) + (($ft20 != null) ? $ft20.hashCode() : 43); final Object $ft20Currency = this.getFt20Currency(); - result = result * PRIME + ($ft20Currency == null ? 43 : $ft20Currency.hashCode()); + result = (result * PRIME) + (($ft20Currency != null) ? $ft20Currency.hashCode() : 43); final Object $ft40 = this.getFt40(); - result = result * PRIME + ($ft40 == null ? 43 : $ft40.hashCode()); + result = (result * PRIME) + (($ft40 != null) ? $ft40.hashCode() : 43); final Object $ft40Currency = this.getFt40Currency(); - result = result * PRIME + ($ft40Currency == null ? 43 : $ft40Currency.hashCode()); + result = (result * PRIME) + (($ft40Currency != null) ? $ft40Currency.hashCode() : 43); final Object $ft40HC = this.getFt40HC(); - result = result * PRIME + ($ft40HC == null ? 43 : $ft40HC.hashCode()); + result = (result * PRIME) + (($ft40HC != null) ? $ft40HC.hashCode() : 43); final Object $ft40HCCurrency = this.getFt40HCCurrency(); - result = result * PRIME + ($ft40HCCurrency == null ? 43 : $ft40HCCurrency.hashCode()); + result = (result * PRIME) + (($ft40HCCurrency != null) ? $ft40HCCurrency.hashCode() : 43); final Object $pod = this.getPod(); - result = result * PRIME + ($pod == null ? 43 : $pod.hashCode()); + result = (result * PRIME) + (($pod != null) ? $pod.hashCode() : 43); return result; } + @Override public String toString() { - return "SameIdProperty.RouteProperties(id=" + this.getId() + ", truck=" + this.getTruck() + ", truckCurrency=" + this.getTruckCurrency() + ", ft20=" + this.getFt20() + ", ft20Currency=" + this.getFt20Currency() + ", ft40=" + this.getFt40() + ", ft40Currency=" + this.getFt40Currency() + ", ft40HC=" + this.getFt40HC() + ", ft40HCCurrency=" + this.getFt40HCCurrency() + ", pod=" + this.getPod() + ")"; + return "SameIdProperty.RouteProperties(id=" + this.getId() + ", truck=" + this.getTruck() + + ", truckCurrency=" + this.getTruckCurrency() + ", ft20=" + this.getFt20() + ", ft20Currency=" + + this.getFt20Currency() + ", ft40=" + this.getFt40() + ", ft40Currency=" + this.getFt40Currency() + + ", ft40HC=" + this.getFt40HC() + ", ft40HCCurrency=" + this.getFt40HCCurrency() + ", pod=" + + this.getPod() + ")"; } - public RouteProperties withId(Long id) { - return this.id == id ? this : new RouteProperties(id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withTruck(Double truck) { - return this.truck == truck ? this : new RouteProperties(this.id, truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withTruckCurrency(String truckCurrency) { - return this.truckCurrency == truckCurrency ? this : new RouteProperties(this.id, this.truck, truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt20(Double ft20) { - return this.ft20 == ft20 ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt20Currency(String ft20Currency) { - return this.ft20Currency == ft20Currency ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt40(Double ft40) { - return this.ft40 == ft40 ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt40Currency(String ft40Currency) { - return this.ft40Currency == ft40Currency ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, ft40Currency, this.ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt40HC(Double ft40HC) { - return this.ft40HC == ft40HC ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, ft40HC, this.ft40HCCurrency, this.pod); - } - - public RouteProperties withFt40HCCurrency(String ft40HCCurrency) { - return this.ft40HCCurrency == ft40HCCurrency ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, ft40HCCurrency, this.pod); - } - - public RouteProperties withPod(PodEntity pod) { - return this.pod == pod ? this : new RouteProperties(this.id, this.truck, this.truckCurrency, this.ft20, this.ft20Currency, this.ft40, this.ft40Currency, this.ft40HC, this.ft40HCCurrency, pod); - } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java index 7fe4ee84a..56297cd89 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.UUID; import org.neo4j.driver.QueryRunner; + import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.schema.CompositeProperty; import org.springframework.data.neo4j.core.schema.GeneratedValue; @@ -36,11 +37,27 @@ import org.springframework.data.neo4j.core.schema.Property; public class ScrollingEntity { /** - * Sorting by b and a will not be unique for 3 and D0, so this will trigger the additional condition based on the id + * Sorting by b and a will not be unique for 3 and D0, so this will trigger the + * additional condition based on the id */ public static final Sort SORT_BY_B_AND_A = Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a")); + public static final Sort SORT_BY_C = Sort.by(Sort.Order.asc("c")); + @Id + @GeneratedValue + private UUID id; + + @Property("foobar") + private String a; + + private Integer b; + + private LocalDateTime c; + + @CompositeProperty + private Map basicComposite; + public static void createTestData(QueryRunner queryRunner) { queryRunner.run("MATCH (n) DETACH DELETE n").consume(); queryRunner.run(""" @@ -69,26 +86,12 @@ public class ScrollingEntity { """).consume(); } - @Id - @GeneratedValue - private UUID id; - - @Property("foobar") - private String a; - - private Integer b; - - private LocalDateTime c; - - @CompositeProperty - private Map basicComposite; - public UUID getId() { - return id; + return this.id; } public String getA() { - return a; + return this.a; } public void setA(String a) { @@ -96,7 +99,7 @@ public class ScrollingEntity { } public Integer getB() { - return b; + return this.b; } public void setB(Integer b) { @@ -104,7 +107,7 @@ public class ScrollingEntity { } public LocalDateTime getC() { - return c; + return this.c; } public void setC(LocalDateTime c) { @@ -113,10 +116,7 @@ public class ScrollingEntity { @Override public String toString() { - return "ScrollingEntity{" + - "a='" + a + '\'' + - ", b=" + b + - ", c=" + c + - '}'; + return "ScrollingEntity{" + "a='" + this.a + '\'' + ", b=" + this.b + ", c=" + this.c + '}'; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimilarThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimilarThing.java index 364dec252..17531d135 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimilarThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimilarThing.java @@ -28,6 +28,7 @@ import org.springframework.data.neo4j.core.schema.Relationship; */ @Node public class SimilarThing { + @Id @GeneratedValue private Long id; @@ -45,7 +46,7 @@ public class SimilarThing { private List noSimilarThings; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -53,7 +54,7 @@ public class SimilarThing { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -75,11 +76,6 @@ public class SimilarThing { this.similarOf = similarOf; } - @Override - public String toString() { - return "Similar{" + "id=" + id + ", name='" + name + '\'' + '}'; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -89,11 +85,17 @@ public class SimilarThing { return false; } SimilarThing similarThing = (SimilarThing) o; - return id.equals(similarThing.id) && name.equals(similarThing.name); + return this.id.equals(similarThing.id) && this.name.equals(similarThing.name); } @Override public int hashCode() { - return Objects.hash(id, name); + return Objects.hash(this.id, this.name); } + + @Override + public String toString() { + return "Similar{" + "id=" + this.id + ", name='" + this.name + '\'' + '}'; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipA.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipA.java index a47d5e750..ef83773e9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipA.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipA.java @@ -15,13 +15,13 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.List; + 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.Relationship; -import java.util.List; - /** * @author Gerrit Meier */ @@ -36,6 +36,7 @@ public class SimpleEntityWithRelationshipA { private List bs; public List getBs() { - return bs; + return this.bs; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipB.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipB.java index ddf2ba0fa..58e0221cf 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipB.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimpleEntityWithRelationshipB.java @@ -36,6 +36,7 @@ public class SimpleEntityWithRelationshipB { private List cs; public List getCs() { - return cs; + return this.cs; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimplePerson.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimplePerson.java index da87160c0..d96829a9a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimplePerson.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SimplePerson.java @@ -36,6 +36,7 @@ public class SimplePerson { } public String getName() { - return name; + return this.name; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SummaryMetric.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SummaryMetric.java index fb1d34c4b..1f6708ded 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/SummaryMetric.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/SummaryMetric.java @@ -26,4 +26,5 @@ public class SummaryMetric extends Metric { public SummaryMetric(String name) { super(name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/TestSequenceGenerator.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/TestSequenceGenerator.java index 56cd78c56..fd9419559 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/TestSequenceGenerator.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/TestSequenceGenerator.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.shared.common; import java.util.concurrent.atomic.AtomicInteger; import org.neo4j.driver.Driver; + import org.springframework.data.neo4j.core.schema.IdGenerator; import org.springframework.util.StringUtils; @@ -31,8 +32,8 @@ public class TestSequenceGenerator implements IdGenerator { private final AtomicInteger sequence = new AtomicInteger(0); /** - * Use an instance of the {@link Driver} bean here to ensure that also injection works when the {@link IdGenerator} - * gets created. + * Use an instance of the {@link Driver} bean here to ensure that also injection works + * when the {@link IdGenerator} gets created. **/ private final Driver driver; @@ -42,6 +43,7 @@ public class TestSequenceGenerator implements IdGenerator { @Override public String generateId(String primaryLabel, Object entity) { - return StringUtils.uncapitalize(primaryLabel) + "-" + sequence.incrementAndGet(); + return StringUtils.uncapitalize(primaryLabel) + "-" + this.sequence.incrementAndGet(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes.java index f38675e79..a939f48a9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.neo4j.driver.types.IsoDuration; -import org.neo4j.driver.types.Point; -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 java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -28,6 +22,14 @@ import java.time.LocalTime; import java.time.OffsetTime; import java.time.Period; import java.time.ZonedDateTime; +import java.util.Objects; + +import org.neo4j.driver.types.IsoDuration; +import org.neo4j.driver.types.Point; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; /** * Contains properties of all cypher types. @@ -36,7 +38,7 @@ import java.time.ZonedDateTime; */ @SuppressWarnings("HiddenField") @Node("CypherTypes") -public class ThingWithAllCypherTypes { +public final class ThingWithAllCypherTypes { @Id @GeneratedValue @@ -70,7 +72,10 @@ public class ThingWithAllCypherTypes { private Duration aZeroDuration; - private ThingWithAllCypherTypes(Long id, boolean aBoolean, long aLong, double aDouble, String aString, byte[] aByteArray, LocalDate aLocalDate, OffsetTime anOffsetTime, LocalTime aLocalTime, ZonedDateTime aZoneDateTime, LocalDateTime aLocalDateTime, IsoDuration anIsoDuration, Point aPoint, Period aZeroPeriod, Duration aZeroDuration) { + private ThingWithAllCypherTypes(Long id, boolean aBoolean, long aLong, double aDouble, String aString, + byte[] aByteArray, LocalDate aLocalDate, OffsetTime anOffsetTime, LocalTime aLocalTime, + ZonedDateTime aZoneDateTime, LocalDateTime aLocalDateTime, IsoDuration anIsoDuration, Point aPoint, + Period aZeroPeriod, Duration aZeroDuration) { this.id = id; this.aBoolean = aBoolean; this.aLong = aLong; @@ -100,114 +105,126 @@ public class ThingWithAllCypherTypes { return this.aBoolean; } - public long getALong() { - return this.aLong; - } - - public double getADouble() { - return this.aDouble; - } - - public String getAString() { - return this.aString; - } - - public byte[] getAByteArray() { - return this.aByteArray; - } - - public LocalDate getALocalDate() { - return this.aLocalDate; - } - - public OffsetTime getAnOffsetTime() { - return this.anOffsetTime; - } - - public LocalTime getALocalTime() { - return this.aLocalTime; - } - - public ZonedDateTime getAZoneDateTime() { - return this.aZoneDateTime; - } - - public LocalDateTime getALocalDateTime() { - return this.aLocalDateTime; - } - - public IsoDuration getAnIsoDuration() { - return this.anIsoDuration; - } - - public Point getAPoint() { - return this.aPoint; - } - - public Period getAZeroPeriod() { - return this.aZeroPeriod; - } - - public Duration getAZeroDuration() { - return this.aZeroDuration; - } - public void setABoolean(boolean aBoolean) { this.aBoolean = aBoolean; } + public long getALong() { + return this.aLong; + } + public void setALong(long aLong) { this.aLong = aLong; } + public double getADouble() { + return this.aDouble; + } + public void setADouble(double aDouble) { this.aDouble = aDouble; } + public String getAString() { + return this.aString; + } + public void setAString(String aString) { this.aString = aString; } + public byte[] getAByteArray() { + return this.aByteArray; + } + public void setAByteArray(byte[] aByteArray) { this.aByteArray = aByteArray; } + public LocalDate getALocalDate() { + return this.aLocalDate; + } + public void setALocalDate(LocalDate aLocalDate) { this.aLocalDate = aLocalDate; } + public OffsetTime getAnOffsetTime() { + return this.anOffsetTime; + } + public void setAnOffsetTime(OffsetTime anOffsetTime) { this.anOffsetTime = anOffsetTime; } + public LocalTime getALocalTime() { + return this.aLocalTime; + } + public void setALocalTime(LocalTime aLocalTime) { this.aLocalTime = aLocalTime; } + public ZonedDateTime getAZoneDateTime() { + return this.aZoneDateTime; + } + public void setAZoneDateTime(ZonedDateTime aZoneDateTime) { this.aZoneDateTime = aZoneDateTime; } + public LocalDateTime getALocalDateTime() { + return this.aLocalDateTime; + } + public void setALocalDateTime(LocalDateTime aLocalDateTime) { this.aLocalDateTime = aLocalDateTime; } + public IsoDuration getAnIsoDuration() { + return this.anIsoDuration; + } + public void setAnIsoDuration(IsoDuration anIsoDuration) { this.anIsoDuration = anIsoDuration; } + public Point getAPoint() { + return this.aPoint; + } + public void setAPoint(Point aPoint) { this.aPoint = aPoint; } + public Period getAZeroPeriod() { + return this.aZeroPeriod; + } + public void setAZeroPeriod(Period aZeroPeriod) { this.aZeroPeriod = aZeroPeriod; } + public Duration getAZeroDuration() { + return this.aZeroDuration; + } + public void setAZeroDuration(Duration aZeroDuration) { this.aZeroDuration = aZeroDuration; } + protected boolean canEqual(final Object other) { + return other instanceof ThingWithAllCypherTypes; + } + + public ThingWithAllCypherTypes withId(Long id) { + return Objects.equals(this.id, id) ? this + : new ThingWithAllCypherTypes(id, this.aBoolean, this.aLong, this.aDouble, this.aString, + this.aByteArray, this.aLocalDate, this.anOffsetTime, this.aLocalTime, this.aZoneDateTime, + this.aLocalDateTime, this.anIsoDuration, this.aPoint, this.aZeroPeriod, this.aZeroDuration); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -221,7 +238,7 @@ public class ThingWithAllCypherTypes { } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } if (this.isABoolean() != other.isABoolean()) { @@ -235,7 +252,7 @@ public class ThingWithAllCypherTypes { } final Object this$aString = this.getAString(); final Object other$aString = other.getAString(); - if (this$aString == null ? other$aString != null : !this$aString.equals(other$aString)) { + if (!Objects.equals(this$aString, other$aString)) { return false; } if (!java.util.Arrays.equals(this.getAByteArray(), other.getAByteArray())) { @@ -243,116 +260,129 @@ public class ThingWithAllCypherTypes { } final Object this$aLocalDate = this.getALocalDate(); final Object other$aLocalDate = other.getALocalDate(); - if (this$aLocalDate == null ? other$aLocalDate != null : !this$aLocalDate.equals(other$aLocalDate)) { + if (!Objects.equals(this$aLocalDate, other$aLocalDate)) { return false; } final Object this$anOffsetTime = this.getAnOffsetTime(); final Object other$anOffsetTime = other.getAnOffsetTime(); - if (this$anOffsetTime == null ? other$anOffsetTime != null : !this$anOffsetTime.equals(other$anOffsetTime)) { + if (!Objects.equals(this$anOffsetTime, other$anOffsetTime)) { return false; } final Object this$aLocalTime = this.getALocalTime(); final Object other$aLocalTime = other.getALocalTime(); - if (this$aLocalTime == null ? other$aLocalTime != null : !this$aLocalTime.equals(other$aLocalTime)) { + if (!Objects.equals(this$aLocalTime, other$aLocalTime)) { return false; } final Object this$aZoneDateTime = this.getAZoneDateTime(); final Object other$aZoneDateTime = other.getAZoneDateTime(); - if (this$aZoneDateTime == null ? other$aZoneDateTime != null : !this$aZoneDateTime.equals(other$aZoneDateTime)) { + if (!Objects.equals(this$aZoneDateTime, other$aZoneDateTime)) { return false; } final Object this$aLocalDateTime = this.getALocalDateTime(); final Object other$aLocalDateTime = other.getALocalDateTime(); - if (this$aLocalDateTime == null ? other$aLocalDateTime != null : !this$aLocalDateTime.equals(other$aLocalDateTime)) { + if (!Objects.equals(this$aLocalDateTime, other$aLocalDateTime)) { return false; } final Object this$anIsoDuration = this.getAnIsoDuration(); final Object other$anIsoDuration = other.getAnIsoDuration(); - if (this$anIsoDuration == null ? other$anIsoDuration != null : !this$anIsoDuration.equals(other$anIsoDuration)) { + if (!Objects.equals(this$anIsoDuration, other$anIsoDuration)) { return false; } final Object this$aPoint = this.getAPoint(); final Object other$aPoint = other.getAPoint(); - if (this$aPoint == null ? other$aPoint != null : !this$aPoint.equals(other$aPoint)) { + if (!Objects.equals(this$aPoint, other$aPoint)) { return false; } final Object this$aZeroPeriod = this.getAZeroPeriod(); final Object other$aZeroPeriod = other.getAZeroPeriod(); - if (this$aZeroPeriod == null ? other$aZeroPeriod != null : !this$aZeroPeriod.equals(other$aZeroPeriod)) { + if (!Objects.equals(this$aZeroPeriod, other$aZeroPeriod)) { return false; } final Object this$aZeroDuration = this.getAZeroDuration(); final Object other$aZeroDuration = other.getAZeroDuration(); - if (this$aZeroDuration == null ? other$aZeroDuration != null : !this$aZeroDuration.equals(other$aZeroDuration)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof ThingWithAllCypherTypes; + return Objects.equals(this$aZeroDuration, other$aZeroDuration); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); result = result * PRIME + (this.isABoolean() ? 79 : 97); final long $aLong = this.getALong(); result = result * PRIME + (int) ($aLong >>> 32 ^ $aLong); final long $aDouble = Double.doubleToLongBits(this.getADouble()); result = result * PRIME + (int) ($aDouble >>> 32 ^ $aDouble); final Object $aString = this.getAString(); - result = result * PRIME + ($aString == null ? 43 : $aString.hashCode()); + result = (result * PRIME) + (($aString != null) ? $aString.hashCode() : 43); result = result * PRIME + java.util.Arrays.hashCode(this.getAByteArray()); final Object $aLocalDate = this.getALocalDate(); - result = result * PRIME + ($aLocalDate == null ? 43 : $aLocalDate.hashCode()); + result = (result * PRIME) + (($aLocalDate != null) ? $aLocalDate.hashCode() : 43); final Object $anOffsetTime = this.getAnOffsetTime(); - result = result * PRIME + ($anOffsetTime == null ? 43 : $anOffsetTime.hashCode()); + result = (result * PRIME) + (($anOffsetTime != null) ? $anOffsetTime.hashCode() : 43); final Object $aLocalTime = this.getALocalTime(); - result = result * PRIME + ($aLocalTime == null ? 43 : $aLocalTime.hashCode()); + result = (result * PRIME) + (($aLocalTime != null) ? $aLocalTime.hashCode() : 43); final Object $aZoneDateTime = this.getAZoneDateTime(); - result = result * PRIME + ($aZoneDateTime == null ? 43 : $aZoneDateTime.hashCode()); + result = (result * PRIME) + (($aZoneDateTime != null) ? $aZoneDateTime.hashCode() : 43); final Object $aLocalDateTime = this.getALocalDateTime(); - result = result * PRIME + ($aLocalDateTime == null ? 43 : $aLocalDateTime.hashCode()); + result = (result * PRIME) + (($aLocalDateTime != null) ? $aLocalDateTime.hashCode() : 43); final Object $anIsoDuration = this.getAnIsoDuration(); - result = result * PRIME + ($anIsoDuration == null ? 43 : $anIsoDuration.hashCode()); + result = (result * PRIME) + (($anIsoDuration != null) ? $anIsoDuration.hashCode() : 43); final Object $aPoint = this.getAPoint(); - result = result * PRIME + ($aPoint == null ? 43 : $aPoint.hashCode()); + result = (result * PRIME) + (($aPoint != null) ? $aPoint.hashCode() : 43); final Object $aZeroPeriod = this.getAZeroPeriod(); - result = result * PRIME + ($aZeroPeriod == null ? 43 : $aZeroPeriod.hashCode()); + result = (result * PRIME) + (($aZeroPeriod != null) ? $aZeroPeriod.hashCode() : 43); final Object $aZeroDuration = this.getAZeroDuration(); - result = result * PRIME + ($aZeroDuration == null ? 43 : $aZeroDuration.hashCode()); + result = (result * PRIME) + (($aZeroDuration != null) ? $aZeroDuration.hashCode() : 43); return result; } + @Override public String toString() { - return "ThingWithAllCypherTypes(id=" + this.getId() + ", aBoolean=" + this.isABoolean() + ", aLong=" + this.getALong() + ", aDouble=" + this.getADouble() + ", aString=" + this.getAString() + ", aByteArray=" + java.util.Arrays.toString(this.getAByteArray()) + ", aLocalDate=" + this.getALocalDate() + ", anOffsetTime=" + this.getAnOffsetTime() + ", aLocalTime=" + this.getALocalTime() + ", aZoneDateTime=" + this.getAZoneDateTime() + ", aLocalDateTime=" + this.getALocalDateTime() + ", anIsoDuration=" + this.getAnIsoDuration() + ", aPoint=" + this.getAPoint() + ", aZeroPeriod=" + this.getAZeroPeriod() + ", aZeroDuration=" + this.getAZeroDuration() + ")"; - } - - public ThingWithAllCypherTypes withId(Long id) { - return this.id == id ? this : new ThingWithAllCypherTypes(id, this.aBoolean, this.aLong, this.aDouble, this.aString, this.aByteArray, this.aLocalDate, this.anOffsetTime, this.aLocalTime, this.aZoneDateTime, this.aLocalDateTime, this.anIsoDuration, this.aPoint, this.aZeroPeriod, this.aZeroDuration); + return "ThingWithAllCypherTypes(id=" + this.getId() + ", aBoolean=" + this.isABoolean() + ", aLong=" + + this.getALong() + ", aDouble=" + this.getADouble() + ", aString=" + this.getAString() + + ", aByteArray=" + java.util.Arrays.toString(this.getAByteArray()) + ", aLocalDate=" + + this.getALocalDate() + ", anOffsetTime=" + this.getAnOffsetTime() + ", aLocalTime=" + + this.getALocalTime() + ", aZoneDateTime=" + this.getAZoneDateTime() + ", aLocalDateTime=" + + this.getALocalDateTime() + ", anIsoDuration=" + this.getAnIsoDuration() + ", aPoint=" + + this.getAPoint() + ", aZeroPeriod=" + this.getAZeroPeriod() + ", aZeroDuration=" + + this.getAZeroDuration() + ")"; } /** * the builder */ public static class ThingWithAllCypherTypesBuilder { + private Long id; + private boolean aBoolean; + private long aLong; + private double aDouble; + private String aString; + private byte[] aByteArray; + private LocalDate aLocalDate; + private OffsetTime anOffsetTime; + private LocalTime aLocalTime; + private ZonedDateTime aZoneDateTime; + private LocalDateTime aLocalDateTime; + private IsoDuration anIsoDuration; + private Point aPoint; + private Period aZeroPeriod; + private Duration aZeroDuration; ThingWithAllCypherTypesBuilder() { @@ -434,11 +464,22 @@ public class ThingWithAllCypherTypes { } public ThingWithAllCypherTypes build() { - return new ThingWithAllCypherTypes(this.id, this.aBoolean, this.aLong, this.aDouble, this.aString, this.aByteArray, this.aLocalDate, this.anOffsetTime, this.aLocalTime, this.aZoneDateTime, this.aLocalDateTime, this.anIsoDuration, this.aPoint, this.aZeroPeriod, this.aZeroDuration); + return new ThingWithAllCypherTypes(this.id, this.aBoolean, this.aLong, this.aDouble, this.aString, + this.aByteArray, this.aLocalDate, this.anOffsetTime, this.aLocalTime, this.aZoneDateTime, + this.aLocalDateTime, this.anIsoDuration, this.aPoint, this.aZeroPeriod, this.aZeroDuration); } + @Override public String toString() { - return "ThingWithAllCypherTypes.ThingWithAllCypherTypesBuilder(id=" + this.id + ", aBoolean=" + this.aBoolean + ", aLong=" + this.aLong + ", aDouble=" + this.aDouble + ", aString=" + this.aString + ", aByteArray=" + java.util.Arrays.toString(this.aByteArray) + ", aLocalDate=" + this.aLocalDate + ", anOffsetTime=" + this.anOffsetTime + ", aLocalTime=" + this.aLocalTime + ", aZoneDateTime=" + this.aZoneDateTime + ", aLocalDateTime=" + this.aLocalDateTime + ", anIsoDuration=" + this.anIsoDuration + ", aPoint=" + this.aPoint + ", aZeroPeriod=" + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + ")"; + return "ThingWithAllCypherTypes.ThingWithAllCypherTypesBuilder(id=" + this.id + ", aBoolean=" + + this.aBoolean + ", aLong=" + this.aLong + ", aDouble=" + this.aDouble + ", aString=" + + this.aString + ", aByteArray=" + java.util.Arrays.toString(this.aByteArray) + ", aLocalDate=" + + this.aLocalDate + ", anOffsetTime=" + this.anOffsetTime + ", aLocalTime=" + this.aLocalTime + + ", aZoneDateTime=" + this.aZoneDateTime + ", aLocalDateTime=" + this.aLocalDateTime + + ", anIsoDuration=" + this.anIsoDuration + ", aPoint=" + this.aPoint + ", aZeroPeriod=" + + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes2.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes2.java index 25c99a43a..f030b8f04 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes2.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllCypherTypes2.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.common; -import org.neo4j.driver.types.IsoDuration; -import org.neo4j.driver.types.Point; -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 java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -29,6 +23,13 @@ import java.time.OffsetTime; import java.time.Period; import java.time.ZonedDateTime; +import org.neo4j.driver.types.IsoDuration; +import org.neo4j.driver.types.Point; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + /** * Similar to {@link ThingWithAllCypherTypes} but using field access. * @@ -84,119 +85,120 @@ public class ThingWithAllCypherTypes2 { return this.aBoolean; } - public long getALong() { - return this.aLong; - } - - public int getAnInt() { - return this.anInt; - } - - public double getADouble() { - return this.aDouble; - } - - public String getAString() { - return this.aString; - } - - public byte[] getAByteArray() { - return this.aByteArray; - } - - public LocalDate getALocalDate() { - return this.aLocalDate; - } - - public OffsetTime getAnOffsetTime() { - return this.anOffsetTime; - } - - public LocalTime getALocalTime() { - return this.aLocalTime; - } - - public ZonedDateTime getAZoneDateTime() { - return this.aZoneDateTime; - } - - public LocalDateTime getALocalDateTime() { - return this.aLocalDateTime; - } - - public IsoDuration getAnIsoDuration() { - return this.anIsoDuration; - } - - public Point getAPoint() { - return this.aPoint; - } - - public Period getAZeroPeriod() { - return this.aZeroPeriod; - } - - public Duration getAZeroDuration() { - return this.aZeroDuration; - } - public void setABoolean(boolean aBoolean) { this.aBoolean = aBoolean; } + public long getALong() { + return this.aLong; + } + public void setALong(long aLong) { this.aLong = aLong; } + public int getAnInt() { + return this.anInt; + } + public void setAnInt(int anInt) { this.anInt = anInt; } + public double getADouble() { + return this.aDouble; + } + public void setADouble(double aDouble) { this.aDouble = aDouble; } + public String getAString() { + return this.aString; + } + public void setAString(String aString) { this.aString = aString; } + public byte[] getAByteArray() { + return this.aByteArray; + } + public void setAByteArray(byte[] aByteArray) { this.aByteArray = aByteArray; } + public LocalDate getALocalDate() { + return this.aLocalDate; + } + public void setALocalDate(LocalDate aLocalDate) { this.aLocalDate = aLocalDate; } + public OffsetTime getAnOffsetTime() { + return this.anOffsetTime; + } + public void setAnOffsetTime(OffsetTime anOffsetTime) { this.anOffsetTime = anOffsetTime; } + public LocalTime getALocalTime() { + return this.aLocalTime; + } + public void setALocalTime(LocalTime aLocalTime) { this.aLocalTime = aLocalTime; } + public ZonedDateTime getAZoneDateTime() { + return this.aZoneDateTime; + } + public void setAZoneDateTime(ZonedDateTime aZoneDateTime) { this.aZoneDateTime = aZoneDateTime; } + public LocalDateTime getALocalDateTime() { + return this.aLocalDateTime; + } + public void setALocalDateTime(LocalDateTime aLocalDateTime) { this.aLocalDateTime = aLocalDateTime; } + public IsoDuration getAnIsoDuration() { + return this.anIsoDuration; + } + public void setAnIsoDuration(IsoDuration anIsoDuration) { this.anIsoDuration = anIsoDuration; } + public Point getAPoint() { + return this.aPoint; + } + public void setAPoint(Point aPoint) { this.aPoint = aPoint; } + public Period getAZeroPeriod() { + return this.aZeroPeriod; + } + public void setAZeroPeriod(Period aZeroPeriod) { this.aZeroPeriod = aZeroPeriod; } + public Duration getAZeroDuration() { + return this.aZeroDuration; + } + public void setAZeroDuration(Duration aZeroDuration) { this.aZeroDuration = aZeroDuration; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllSpatialTypes.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllSpatialTypes.java index 099b0b5d7..e4296e74a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllSpatialTypes.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAllSpatialTypes.java @@ -15,6 +15,8 @@ */ package org.springframework.data.neo4j.integration.shared.common; +import java.util.Objects; + import org.springframework.data.geo.Point; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -30,7 +32,7 @@ import org.springframework.data.neo4j.types.GeographicPoint3d; * @author Michael J. Simons */ @Node("SpatialTypes") -public class ThingWithAllSpatialTypes { +public final class ThingWithAllSpatialTypes { @Id @GeneratedValue @@ -46,7 +48,8 @@ public class ThingWithAllSpatialTypes { private CartesianPoint3d car3d; - private ThingWithAllSpatialTypes(Long id, Point sdnPoint, GeographicPoint2d geo2d, GeographicPoint3d geo3d, CartesianPoint2d car2d, CartesianPoint3d car3d) { + private ThingWithAllSpatialTypes(Long id, Point sdnPoint, GeographicPoint2d geo2d, GeographicPoint3d geo3d, + CartesianPoint2d car2d, CartesianPoint3d car3d) { this.id = id; this.sdnPoint = sdnPoint; this.geo2d = geo2d; @@ -67,42 +70,52 @@ public class ThingWithAllSpatialTypes { return this.sdnPoint; } - public GeographicPoint2d getGeo2d() { - return this.geo2d; - } - - public GeographicPoint3d getGeo3d() { - return this.geo3d; - } - - public CartesianPoint2d getCar2d() { - return this.car2d; - } - - public CartesianPoint3d getCar3d() { - return this.car3d; - } - public void setSdnPoint(Point sdnPoint) { this.sdnPoint = sdnPoint; } + public GeographicPoint2d getGeo2d() { + return this.geo2d; + } + public void setGeo2d(GeographicPoint2d geo2d) { this.geo2d = geo2d; } + public GeographicPoint3d getGeo3d() { + return this.geo3d; + } + public void setGeo3d(GeographicPoint3d geo3d) { this.geo3d = geo3d; } + public CartesianPoint2d getCar2d() { + return this.car2d; + } + public void setCar2d(CartesianPoint2d car2d) { this.car2d = car2d; } + public CartesianPoint3d getCar3d() { + return this.car3d; + } + public void setCar3d(CartesianPoint3d car3d) { this.car3d = car3d; } + protected boolean canEqual(final Object other) { + return other instanceof ThingWithAllSpatialTypes; + } + + public ThingWithAllSpatialTypes withId(Long newId) { + return Objects.equals(this.id, newId) ? this + : new ThingWithAllSpatialTypes(newId, this.sdnPoint, this.geo2d, this.geo3d, this.car2d, this.car3d); + } + + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -111,70 +124,63 @@ public class ThingWithAllSpatialTypes { return false; } final ThingWithAllSpatialTypes other = (ThingWithAllSpatialTypes) o; - if (!other.canEqual((Object) this)) { + if (!other.canEqual(this)) { return false; } final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$sdnPoint = this.getSdnPoint(); final Object other$sdnPoint = other.getSdnPoint(); - if (this$sdnPoint == null ? other$sdnPoint != null : !this$sdnPoint.equals(other$sdnPoint)) { + if (!Objects.equals(this$sdnPoint, other$sdnPoint)) { return false; } final Object this$geo2d = this.getGeo2d(); final Object other$geo2d = other.getGeo2d(); - if (this$geo2d == null ? other$geo2d != null : !this$geo2d.equals(other$geo2d)) { + if (!Objects.equals(this$geo2d, other$geo2d)) { return false; } final Object this$geo3d = this.getGeo3d(); final Object other$geo3d = other.getGeo3d(); - if (this$geo3d == null ? other$geo3d != null : !this$geo3d.equals(other$geo3d)) { + if (!Objects.equals(this$geo3d, other$geo3d)) { return false; } final Object this$car2d = this.getCar2d(); final Object other$car2d = other.getCar2d(); - if (this$car2d == null ? other$car2d != null : !this$car2d.equals(other$car2d)) { + if (!Objects.equals(this$car2d, other$car2d)) { return false; } final Object this$car3d = this.getCar3d(); final Object other$car3d = other.getCar3d(); - if (this$car3d == null ? other$car3d != null : !this$car3d.equals(other$car3d)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof ThingWithAllSpatialTypes; + return Objects.equals(this$car3d, other$car3d); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $sdnPoint = this.getSdnPoint(); - result = result * PRIME + ($sdnPoint == null ? 43 : $sdnPoint.hashCode()); + result = (result * PRIME) + (($sdnPoint != null) ? $sdnPoint.hashCode() : 43); final Object $geo2d = this.getGeo2d(); - result = result * PRIME + ($geo2d == null ? 43 : $geo2d.hashCode()); + result = (result * PRIME) + (($geo2d != null) ? $geo2d.hashCode() : 43); final Object $geo3d = this.getGeo3d(); - result = result * PRIME + ($geo3d == null ? 43 : $geo3d.hashCode()); + result = (result * PRIME) + (($geo3d != null) ? $geo3d.hashCode() : 43); final Object $car2d = this.getCar2d(); - result = result * PRIME + ($car2d == null ? 43 : $car2d.hashCode()); + result = (result * PRIME) + (($car2d != null) ? $car2d.hashCode() : 43); final Object $car3d = this.getCar3d(); - result = result * PRIME + ($car3d == null ? 43 : $car3d.hashCode()); + result = (result * PRIME) + (($car3d != null) ? $car3d.hashCode() : 43); return result; } + @Override public String toString() { - return "ThingWithAllSpatialTypes(id=" + this.getId() + ", sdnPoint=" + this.getSdnPoint() + ", geo2d=" + this.getGeo2d() + ", geo3d=" + this.getGeo3d() + ", car2d=" + this.getCar2d() + ", car3d=" + this.getCar3d() + ")"; - } - - public ThingWithAllSpatialTypes withId(Long newId) { - return this.id == newId ? this : new ThingWithAllSpatialTypes(newId, this.sdnPoint, this.geo2d, this.geo3d, this.car2d, this.car3d); + return "ThingWithAllSpatialTypes(id=" + this.getId() + ", sdnPoint=" + this.getSdnPoint() + ", geo2d=" + + this.getGeo2d() + ", geo3d=" + this.getGeo3d() + ", car2d=" + this.getCar2d() + ", car3d=" + + this.getCar3d() + ")"; } /** @@ -182,11 +188,17 @@ public class ThingWithAllSpatialTypes { */ @SuppressWarnings("HiddenField") public static class ThingWithAllSpatialTypesBuilder { + private Long id; + private Point sdnPoint; + private GeographicPoint2d geo2d; + private GeographicPoint3d geo3d; + private CartesianPoint2d car2d; + private CartesianPoint3d car3d; ThingWithAllSpatialTypesBuilder() { @@ -226,8 +238,13 @@ public class ThingWithAllSpatialTypes { return new ThingWithAllSpatialTypes(this.id, this.sdnPoint, this.geo2d, this.geo3d, this.car2d, this.car3d); } + @Override public String toString() { - return "ThingWithAllSpatialTypes.ThingWithAllSpatialTypesBuilder(id=" + this.id + ", sdnPoint=" + this.sdnPoint + ", geo2d=" + this.geo2d + ", geo3d=" + this.geo3d + ", car2d=" + this.car2d + ", car3d=" + this.car3d + ")"; + return "ThingWithAllSpatialTypes.ThingWithAllSpatialTypesBuilder(id=" + this.id + ", sdnPoint=" + + this.sdnPoint + ", geo2d=" + this.geo2d + ", geo3d=" + this.geo3d + ", car2d=" + this.car2d + + ", car3d=" + this.car3d + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAssignedId.java index d4805a5b1..1dda5ddad 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithAssignedId.java @@ -50,11 +50,11 @@ public class ThingWithAssignedId extends AbstractNamedThing { } public String getTheId() { - return theId; + return this.theId; } public List getThings() { - return things; + return this.things; } public void setThings(List things) { @@ -62,7 +62,7 @@ public class ThingWithAssignedId extends AbstractNamedThing { } public String getRandomValue() { - return randomValue; + return this.randomValue; } public void setRandomValue(String randomValue) { @@ -70,11 +70,12 @@ public class ThingWithAssignedId extends AbstractNamedThing { } public String getAnotherRandomValue() { - return anotherRandomValue; + return this.anotherRandomValue; } @PostLoad public void generateValue() { this.anotherRandomValue = UUID.randomUUID().toString(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithFixedGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithFixedGeneratedId.java index 59aaaa820..14b34a7f8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithFixedGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithFixedGeneratedId.java @@ -38,10 +38,11 @@ public class ThingWithFixedGeneratedId extends AbstractNamedThing { } public String getTheId() { - return theId; + return this.theId; } public void setPerson(SimplePerson person) { this.person = person; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithGeneratedId.java index 51370a417..5271c1d1b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithGeneratedId.java @@ -34,6 +34,7 @@ public class ThingWithGeneratedId extends AbstractNamedThing { } public String getTheId() { - return theId; + return this.theId; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithIdGeneratedByBean.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithIdGeneratedByBean.java index 461d5937a..c53c2ca15 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithIdGeneratedByBean.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithIdGeneratedByBean.java @@ -34,6 +34,7 @@ public class ThingWithIdGeneratedByBean extends AbstractNamedThing { } public String getTheId() { - return theId; + return this.theId; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithSequence.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithSequence.java index 1885b38a4..1c3c35717 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithSequence.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithSequence.java @@ -39,10 +39,11 @@ public class ThingWithSequence { } public String getName() { - return name; + return this.name; } public Long getSequenceNumber() { - return sequenceNumber; + return this.sequenceNumber; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithUUIDID.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithUUIDID.java index d97e33200..57378f4a7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithUUIDID.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithUUIDID.java @@ -23,7 +23,6 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons - * @soundtrack Samy Deluxe - Samy Deluxe */ @Node public class ThingWithUUIDID { @@ -41,18 +40,19 @@ public class ThingWithUUIDID { } public UUID getId() { - return id; + return this.id; } public String getName() { - return name; + return this.name; } public ThingWithUUIDID getAnotherThing() { - return anotherThing; + return this.anotherThing; } public void setAnotherThing(ThingWithUUIDID anotherThing) { this.anotherThing = anotherThing; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThing.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThing.java index 937b86195..728c2c8db 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThing.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThing.java @@ -30,6 +30,8 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node public class VersionedThing { + private final String name; + @Id @GeneratedValue private Long id; @@ -37,8 +39,6 @@ public class VersionedThing { @Version private Long myVersion; - private final String name; - private String mutableProperty; @Relationship("HAS") @@ -49,11 +49,11 @@ public class VersionedThing { } public Long getId() { - return id; + return this.id; } public Long getMyVersion() { - return myVersion; + return this.myVersion; } public void setMyVersion(Long myVersion) { @@ -61,7 +61,7 @@ public class VersionedThing { } public List getOtherVersionedThings() { - return otherVersionedThings; + return this.otherVersionedThings; } public void setOtherVersionedThings(List otherVersionedThings) { @@ -69,7 +69,7 @@ public class VersionedThing { } public String getMutableProperty() { - return mutableProperty; + return this.mutableProperty; } public void setMutableProperty(String mutableProperty) { @@ -85,11 +85,12 @@ public class VersionedThing { return false; } VersionedThing that = (VersionedThing) o; - return Objects.equals(id, that.id) && name.equals(that.name); + return Objects.equals(this.id, that.id) && this.name.equals(that.name); } @Override public int hashCode() { - return Objects.hash(id, name); + return Objects.hash(this.id, this.name); } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThingWithAssignedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThingWithAssignedId.java index ee009f204..0476f30c9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThingWithAssignedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/VersionedThingWithAssignedId.java @@ -31,11 +31,11 @@ public class VersionedThingWithAssignedId { @Id private final Long id; + private final String name; + @Version private Long myVersion; - private final String name; - @Relationship("HAS") private List otherVersionedThings; @@ -45,7 +45,7 @@ public class VersionedThingWithAssignedId { } public Long getMyVersion() { - return myVersion; + return this.myVersion; } public void setMyVersion(Long myVersion) { @@ -53,10 +53,11 @@ public class VersionedThingWithAssignedId { } public List getOtherVersionedThings() { - return otherVersionedThings; + return this.otherVersionedThings; } public void setOtherVersionedThings(List otherVersionedThings) { this.otherVersionedThings = otherVersionedThings; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/WorksInClubRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/WorksInClubRelationship.java index 020d37bc9..1597cb11d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/WorksInClubRelationship.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/WorksInClubRelationship.java @@ -25,24 +25,25 @@ import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class WorksInClubRelationship { - @RelationshipId - private Long id; - private final Integer since; @TargetNode private final Club club; + @RelationshipId + private Long id; + public WorksInClubRelationship(Integer since, Club club) { this.since = since; this.club = club; } public Integer getSince() { - return since; + return this.since; } public Club getClub() { - return club; + return this.club; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/CompositePropertiesITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/CompositePropertiesITBase.java index 3e6b428de..bdbcff145 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/CompositePropertiesITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/CompositePropertiesITBase.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.conversion; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.LocalDate; import java.util.Collections; import java.util.HashMap; @@ -28,29 +26,31 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Relationship; + import org.springframework.data.neo4j.integration.shared.common.Club; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Learning English, Lesson Two */ public abstract class CompositePropertiesITBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - private final Function, String> newKey = e -> e.getKey() - .substring(e.getKey().indexOf(".") + 1); - protected final Driver driver; - private final BookmarkCapture bookmarkCapture; - protected final Map nodeProperties; protected final Map relationshipProperties; + private final Function, String> newKey = e -> e.getKey() + .substring(e.getKey().indexOf(".") + 1); + + private final BookmarkCapture bookmarkCapture; + protected CompositePropertiesITBase(Driver driver, BookmarkCapture bookmarkCapture) { this.driver = driver; @@ -80,35 +80,39 @@ public abstract class CompositePropertiesITBase { properties.put("dto.x", "A"); properties.put("dto.y", 1L); properties.put("dto.z", 4.2); - nodeProperties = Collections.unmodifiableMap(properties); + this.nodeProperties = Collections.unmodifiableMap(properties); properties = new HashMap<>(); properties.put("someProperties.a", "B"); properties.put("dto.x", "B"); properties.put("dto.y", 10L); properties.put("dto.z", 42.0); - relationshipProperties = Collections.unmodifiableMap(properties); + this.relationshipProperties = Collections.unmodifiableMap(properties); } protected long createNodeWithCompositeProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { long id = session.executeWrite( - tx -> tx.run("CREATE (t:CompositeProperties) SET t = $properties RETURN id(t)", - Collections.singletonMap("properties", nodeProperties)).single().get(0) - .asLong()); - bookmarkCapture.seedWith(session.lastBookmarks()); + tx -> tx + .run("CREATE (t:CompositeProperties) SET t = $properties RETURN id(t)", + Collections.singletonMap("properties", this.nodeProperties)) + .single() + .get(0) + .asLong()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); return id; } } protected long createRelationshipWithCompositeProperties() { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - long id = session.executeWrite( - tx -> tx.run( - "CREATE (t:CompositeProperties) -[r:IRRELEVANT_TYPE] -> (:Club) SET r = $properties RETURN id(t)", - Collections.singletonMap("properties", relationshipProperties)).single().get(0) - .asLong()); - bookmarkCapture.seedWith(session.lastBookmarks()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + long id = session.executeWrite(tx -> tx + .run("CREATE (t:CompositeProperties) -[r:IRRELEVANT_TYPE] -> (:Club) SET r = $properties RETURN id(t)", + Collections.singletonMap("properties", this.relationshipProperties)) + .single() + .get(0) + .asLong()); + this.bookmarkCapture.seedWith(session.lastBookmarks()); return id; } } @@ -117,40 +121,47 @@ public abstract class CompositePropertiesITBase { ThingWithCompositeProperties t = new ThingWithCompositeProperties(); Map someDates = new HashMap<>(); - nodeProperties.entrySet().stream().filter(e -> e.getKey().startsWith("someDates.")) - .forEach(e -> someDates.put(newKey.apply(e), (LocalDate) e.getValue())); + this.nodeProperties.entrySet() + .stream() + .filter(e -> e.getKey().startsWith("someDates.")) + .forEach(e -> someDates.put(this.newKey.apply(e), (LocalDate) e.getValue())); t.setSomeDates(someDates); - Map someOtherDates = Collections - .singletonMap("t", (LocalDate) nodeProperties.get("in_another_time.t")); + Map someOtherDates = Collections.singletonMap("t", + (LocalDate) this.nodeProperties.get("in_another_time.t")); t.setSomeOtherDates(someOtherDates); Map someCustomThings = new HashMap<>(); - nodeProperties.entrySet().stream().filter(e -> e.getKey().startsWith("customTypeMap.")) - .forEach(e -> someCustomThings - .put(newKey.apply(e), ThingWithCustomTypes.CustomType.of((String) e.getValue()))); + this.nodeProperties.entrySet() + .stream() + .filter(e -> e.getKey().startsWith("customTypeMap.")) + .forEach(e -> someCustomThings.put(this.newKey.apply(e), + ThingWithCustomTypes.CustomType.of((String) e.getValue()))); t.setCustomTypeMap(someCustomThings); Map someDatesByEnumA = new HashMap<>(); - nodeProperties.entrySet().stream().filter(e -> e.getKey().startsWith("someDatesByEnumA.")) - .forEach(e -> someDatesByEnumA - .put(ThingWithCompositeProperties.EnumA.valueOf(newKey.apply(e)), (LocalDate) e.getValue())); + this.nodeProperties.entrySet() + .stream() + .filter(e -> e.getKey().startsWith("someDatesByEnumA.")) + .forEach(e -> someDatesByEnumA.put(ThingWithCompositeProperties.EnumA.valueOf(this.newKey.apply(e)), + (LocalDate) e.getValue())); t.setSomeDatesByEnumA(someDatesByEnumA); Map someDatesByEnumB = new HashMap<>(); - nodeProperties.entrySet().stream().filter(e -> e.getKey().startsWith("someDatesByEnumB.")) - .forEach(e -> someDatesByEnumB - .put(ThingWithCompositeProperties.EnumB.valueOf(newKey.apply(e)), (LocalDate) e.getValue())); + this.nodeProperties.entrySet() + .stream() + .filter(e -> e.getKey().startsWith("someDatesByEnumB.")) + .forEach(e -> someDatesByEnumB.put(ThingWithCompositeProperties.EnumB.valueOf(this.newKey.apply(e)), + (LocalDate) e.getValue())); t.setSomeDatesByEnumB(someDatesByEnumB); - t.setDatesWithTransformedKey(Collections.singletonMap("TEST", (LocalDate) nodeProperties.get("datesWithTransformedKey.test"))); - t.setDatesWithTransformedKeyAndEnum(Collections.singletonMap(ThingWithCompositeProperties.EnumB.VALUE_BA, (LocalDate) nodeProperties.get("datesWithTransformedKeyAndEnum.value_ba"))); + t.setDatesWithTransformedKey( + Collections.singletonMap("TEST", (LocalDate) this.nodeProperties.get("datesWithTransformedKey.test"))); + t.setDatesWithTransformedKeyAndEnum(Collections.singletonMap(ThingWithCompositeProperties.EnumB.VALUE_BA, + (LocalDate) this.nodeProperties.get("datesWithTransformedKeyAndEnum.value_ba"))); - t.setSomeOtherDTO(new ThingWithCompositeProperties.SomeOtherDTO( - (String) nodeProperties.get("dto.x"), - (Long) nodeProperties.get("dto.y"), - (Double) nodeProperties.get("dto.z") - )); + t.setSomeOtherDTO(new ThingWithCompositeProperties.SomeOtherDTO((String) this.nodeProperties.get("dto.x"), + (Long) this.nodeProperties.get("dto.y"), (Double) this.nodeProperties.get("dto.z"))); return t; } @@ -162,16 +173,16 @@ public abstract class CompositePropertiesITBase { target); Map setSomeProperties = new HashMap<>(); - nodeProperties.entrySet().stream().filter(e -> e.getKey().startsWith("someProperties.")) - .forEach(e -> setSomeProperties.put(newKey.apply(e), (String) e.getValue())); + this.nodeProperties.entrySet() + .stream() + .filter(e -> e.getKey().startsWith("someProperties.")) + .forEach(e -> setSomeProperties.put(this.newKey.apply(e), (String) e.getValue())); relationshipWithCompositeProperties.setSomeProperties(setSomeProperties); relationshipWithCompositeProperties.setSomeProperties(Collections.singletonMap("a", "B")); relationshipWithCompositeProperties.setSomeOtherDTO(new ThingWithCompositeProperties.SomeOtherDTO( - (String) relationshipProperties.get("dto.x"), - (Long) relationshipProperties.get("dto.y"), - (Double) relationshipProperties.get("dto.z") - )); + (String) this.relationshipProperties.get("dto.x"), (Long) this.relationshipProperties.get("dto.y"), + (Double) this.relationshipProperties.get("dto.z"))); source.setRelationship(relationshipWithCompositeProperties); return source; @@ -195,17 +206,18 @@ public abstract class CompositePropertiesITBase { mergedProperties.put("dto.y", t.getSomeOtherDTO().y); mergedProperties.put("dto.z", t.getSomeOtherDTO().z); - assertThat(mergedProperties).containsExactlyInAnyOrderEntriesOf(nodeProperties); + assertThat(mergedProperties).containsExactlyInAnyOrderEntriesOf(this.nodeProperties); } protected void assertNodePropertiesInGraph(long id) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - Record r = session.executeRead(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", - Collections.singletonMap("id", id)).single()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + Record r = session.executeRead(tx -> tx + .run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t", Collections.singletonMap("id", id)) + .single()); Node n = r.get("t").asNode(); - assertThat(n.asMap()).containsExactlyInAnyOrderEntriesOf(nodeProperties); - bookmarkCapture.seedWith(session.lastBookmarks()); + assertThat(n.asMap()).containsExactlyInAnyOrderEntriesOf(this.nodeProperties); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -226,13 +238,16 @@ public abstract class CompositePropertiesITBase { protected void assertRelationshipPropertiesInGraph(long id) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { Record r = session.executeRead( - tx -> tx.run("MATCH (t:CompositeProperties) - [r:IRRELEVANT_TYPE] -> () WHERE id(t) = $id RETURN r", - Collections.singletonMap("id", id)).single()); + tx -> tx + .run("MATCH (t:CompositeProperties) - [r:IRRELEVANT_TYPE] -> () WHERE id(t) = $id RETURN r", + Collections.singletonMap("id", id)) + .single()); Relationship rel = r.get("r").asRelationship(); - assertThat(rel.asMap()).containsExactlyInAnyOrderEntriesOf(relationshipProperties); - bookmarkCapture.seedWith(session.lastBookmarks()); + assertThat(rel.asMap()).containsExactlyInAnyOrderEntriesOf(this.relationshipProperties); + this.bookmarkCapture.seedWith(session.lastBookmarks()); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ListPropertyConversionIT.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ListPropertyConversionIT.java index ffb515db4..e56ffc9d6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ListPropertyConversionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ListPropertyConversionIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.conversion; -import static org.assertj.core.api.Assertions.assertThat; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -34,10 +32,10 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.convert.ConvertWith; @@ -54,10 +52,13 @@ import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test around collections to be converted as a whole and not as individual elements. * @@ -76,11 +77,12 @@ class ListPropertyConversionIT { try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { session.run("MATCH (n) DETACH DELETE n").consume(); existingNodeId = session.run("CREATE (n:DomainObjectWithListOfConvertables {" - + "someprefixA_0: '1', someprefixB_0: '2', someprefixA_1: '3', someprefixB_1: '4', " - + "`moreCollectedData.A_0`: '11', `moreCollectedData.B_0`: '22', `moreCollectedData.A_1`: '33', `moreCollectedData.B_1`: '44', " - + "anotherSet: '1;2,3;4'" - + "}) RETURN id(n)") - .single().get(0).asLong(); + + "someprefixA_0: '1', someprefixB_0: '2', someprefixA_1: '3', someprefixB_1: '4', " + + "`moreCollectedData.A_0`: '11', `moreCollectedData.B_0`: '22', `moreCollectedData.A_1`: '33', `moreCollectedData.B_1`: '44', " + + "anotherSet: '1;2,3;4'" + "}) RETURN id(n)") + .single() + .get(0) + .asLong(); bookmarkCapture.seedWith(session.lastBookmarks()); } } @@ -93,15 +95,17 @@ class ListPropertyConversionIT { object.collectedData = Arrays.asList( new SomeConvertableClass(new BigDecimal("523.6"), new BigDecimal("67689.7")), new SomeConvertableClass(new BigDecimal("4456.3"), new BigDecimal("3109.6")), - new SomeConvertableClass(new BigDecimal("100.6"), new BigDecimal("3050.6")) - ); + new SomeConvertableClass(new BigDecimal("100.6"), new BigDecimal("3050.6"))); object = template.save(object); try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - org.neo4j.driver.types.Node node = - session.run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", - Collections.singletonMap("id", object.id)).single().get(0).asNode(); + org.neo4j.driver.types.Node node = session + .run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", + Collections.singletonMap("id", object.id)) + .single() + .get(0) + .asNode(); assertThat(node.get("someprefixA_0").asString()).isEqualTo("523.6"); assertThat(node.get("someprefixA_1").asString()).isEqualTo("4456.3"); @@ -118,17 +122,18 @@ class ListPropertyConversionIT { @Autowired BookmarkCapture bookmarkCapture) { DomainObjectWithListOfConvertables object = new DomainObjectWithListOfConvertables(); - object.moreCollectedData = Arrays.asList( - new SomeConvertableClass(new BigDecimal("42"), new BigDecimal("23")), - new SomeConvertableClass(new BigDecimal("666"), new BigDecimal("665")) - ); + object.moreCollectedData = Arrays.asList(new SomeConvertableClass(new BigDecimal("42"), new BigDecimal("23")), + new SomeConvertableClass(new BigDecimal("666"), new BigDecimal("665"))); object = template.save(object); try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - org.neo4j.driver.types.Node node = - session.run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", - Collections.singletonMap("id", object.id)).single().get(0).asNode(); + org.neo4j.driver.types.Node node = session + .run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", + Collections.singletonMap("id", object.id)) + .single() + .get(0) + .asNode(); assertThat(node.get("moreCollectedData.A_0").asString()).isEqualTo("42"); assertThat(node.get("moreCollectedData.A_1").asString()).isEqualTo("666"); @@ -143,17 +148,18 @@ class ListPropertyConversionIT { @Autowired BookmarkCapture bookmarkCapture) { DomainObjectWithListOfConvertables object = new DomainObjectWithListOfConvertables(); - object.anotherSet = Arrays.asList( - new SomeConvertableClass(new BigDecimal("523.6"), new BigDecimal("67689.7")), + object.anotherSet = Arrays.asList(new SomeConvertableClass(new BigDecimal("523.6"), new BigDecimal("67689.7")), new SomeConvertableClass(new BigDecimal("4456.3"), new BigDecimal("3109.6")), - new SomeConvertableClass(new BigDecimal("100.6"), new BigDecimal("3050.6")) - ); + new SomeConvertableClass(new BigDecimal("100.6"), new BigDecimal("3050.6"))); object = template.save(object); try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - org.neo4j.driver.types.Node node = - session.run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", - Collections.singletonMap("id", object.id)).single().get(0).asNode(); + org.neo4j.driver.types.Node node = session + .run("MATCH (n:DomainObjectWithListOfConvertables) WHERE id(n) = $id RETURN n", + Collections.singletonMap("id", object.id)) + .single() + .get(0) + .asNode(); assertThat(node.get("anotherSet").asString()).isEqualTo("523.6;67689.7,4456.3;3109.6,100.6;3050.6"); } } @@ -168,8 +174,7 @@ class ListPropertyConversionIT { assertThat(object.collectedData).hasSize(2); assertThat(object.collectedData).containsExactlyInAnyOrder( new SomeConvertableClass(new BigDecimal("1"), new BigDecimal("2")), - new SomeConvertableClass(new BigDecimal("3"), new BigDecimal("4")) - ); + new SomeConvertableClass(new BigDecimal("3"), new BigDecimal("4"))); }); } @@ -183,8 +188,7 @@ class ListPropertyConversionIT { assertThat(object.moreCollectedData).hasSize(2); assertThat(object.moreCollectedData).containsExactlyInAnyOrder( new SomeConvertableClass(new BigDecimal("11"), new BigDecimal("22")), - new SomeConvertableClass(new BigDecimal("33"), new BigDecimal("44")) - ); + new SomeConvertableClass(new BigDecimal("33"), new BigDecimal("44"))); }); } @@ -198,15 +202,15 @@ class ListPropertyConversionIT { assertThat(object.anotherSet).hasSize(2); assertThat(object.anotherSet).containsExactlyInAnyOrder( new SomeConvertableClass(new BigDecimal("1"), new BigDecimal("2")), - new SomeConvertableClass(new BigDecimal("3"), new BigDecimal("4")) - ); + new SomeConvertableClass(new BigDecimal("3"), new BigDecimal("4"))); }); } @Node static class DomainObjectWithListOfConvertables { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @CompositeProperty(converter = ListDecomposingConverter.class, delimiter = "", prefix = "someprefix") @@ -217,11 +221,13 @@ class ListPropertyConversionIT { @CompositeProperty(converterRef = "listDecomposingConverterBean") private List moreCollectedData; + } static class SomeConvertableClass { private final BigDecimal x; + private final BigDecimal y; SomeConvertableClass(BigDecimal x, BigDecimal y) { @@ -229,15 +235,16 @@ class ListPropertyConversionIT { this.y = y; } - public BigDecimal getX() { - return x; + BigDecimal getX() { + return this.x; } - public BigDecimal getY() { - return y; + BigDecimal getY() { + return this.y; } - @Override public boolean equals(Object o) { + @Override + public boolean equals(Object o) { if (this == o) { return true; } @@ -245,16 +252,18 @@ class ListPropertyConversionIT { return false; } SomeConvertableClass that = (SomeConvertableClass) o; - return x.equals(that.x) && y.equals(that.y); + return this.x.equals(that.x) && this.y.equals(that.y); } - @Override public int hashCode() { - return Objects.hash(x, y); + @Override + public int hashCode() { + return Objects.hash(this.x, this.y); } + } - static class ListDecomposingConverter implements - Neo4jPersistentPropertyToMapConverter> { + static class ListDecomposingConverter + implements Neo4jPersistentPropertyToMapConverter> { @Override public Map decompose(List property, @@ -286,27 +295,27 @@ class ListPropertyConversionIT { List result = new ArrayList<>(source.size() / 2); for (int i = 0; i < source.size() / 2; ++i) { - result.add(new SomeConvertableClass( - new BigDecimal(source.get("A_" + i).asString()), - new BigDecimal(source.get("B_" + i).asString()) - )); + result.add(new SomeConvertableClass(new BigDecimal(source.get("A_" + i).asString()), + new BigDecimal(source.get("B_" + i).asString()))); } return result; } + } static class SomeconvertableClassConverter implements Neo4jPersistentPropertyConverter> { - @Override public Value write(List source) { + @Override + public Value write(List source) { if (source == null) { return Values.NULL; } - return Values.value(source.stream().map(v -> - String.format("%s;%s", v.x.toString(), v.y.toString())) - .collect(Collectors.joining(","))); + return Values.value(source.stream() + .map(v -> String.format("%s;%s", v.x.toString(), v.y.toString())) + .collect(Collectors.joining(","))); } @Override @@ -317,6 +326,7 @@ class ListPropertyConversionIT { return new SomeConvertableClass(new BigDecimal(pair[0]), new BigDecimal(pair[1])); }).collect(Collectors.toList()); } + } @Configuration @@ -324,6 +334,7 @@ class ListPropertyConversionIT { static class Config extends Neo4jImperativeTestConfiguration { @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); } @@ -338,12 +349,12 @@ class ListPropertyConversionIT { } @Bean - public ListDecomposingConverter listDecomposingConverterBean() { + ListDecomposingConverter listDecomposingConverterBean() { return new ListDecomposingConverter(); } @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -360,5 +371,7 @@ class ListPropertyConversionIT { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java index 7a1492c1e..fd5cf04ba 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java @@ -15,19 +15,6 @@ */ package org.springframework.data.neo4j.integration.shared.conversion; -import org.junit.jupiter.api.BeforeAll; -import org.neo4j.driver.Session; -import org.neo4j.driver.Values; -import org.springframework.data.domain.Vector; -import org.springframework.data.geo.Point; -import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes.SomeEnum; -import org.springframework.data.neo4j.test.BookmarkCapture; -import org.springframework.data.neo4j.test.Neo4jExtension; -import org.springframework.data.neo4j.types.CartesianPoint2d; -import org.springframework.data.neo4j.types.CartesianPoint3d; -import org.springframework.data.neo4j.types.GeographicPoint2d; -import org.springframework.data.neo4j.types.GeographicPoint3d; - import java.math.BigDecimal; import java.math.BigInteger; import java.net.MalformedURLException; @@ -55,6 +42,20 @@ import java.util.Map; import java.util.TimeZone; import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.neo4j.driver.Session; +import org.neo4j.driver.Values; + +import org.springframework.data.domain.Vector; +import org.springframework.data.geo.Point; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes.SomeEnum; +import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.types.CartesianPoint2d; +import org.springframework.data.neo4j.types.CartesianPoint3d; +import org.springframework.data.neo4j.types.GeographicPoint2d; +import org.springframework.data.neo4j.types.GeographicPoint3d; + /** * Provides some nodes spotting properties of all types we support. * @@ -62,11 +63,44 @@ import java.util.UUID; */ public abstract class Neo4jConversionsITBase { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - protected static final BookmarkCapture bookmarkCapture = new BookmarkCapture(); protected static final Map CYPHER_TYPES; + + protected static final Map ADDITIONAL_TYPES; + + protected static final Map SPATIAL_TYPES; + + protected static final Map CUSTOM_TYPES; + + private static final ParamHolder NEO_HQ = ParamHolder.builder() + .latitude(55.612191) + .longitude(12.994823) + .name("Neo4j HQ") + .build(); + + private static final ParamHolder CLARION = ParamHolder.builder() + .latitude(55.607726) + .longitude(12.994243) + .name("Clarion") + .build(); + + private static final ParamHolder MINC = ParamHolder.builder() + .latitude(55.611496) + .longitude(12.994039) + .name("Minc") + .build(); + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + protected static long ID_OF_CYPHER_TYPES_NODE; + + protected static long ID_OF_ADDITIONAL_TYPES_NODE; + + protected static long ID_OF_SPATIAL_TYPES_NODE; + + protected static long ID_OF_CUSTOM_TYPE_NODE; + static { Map hlp = new HashMap<>(); hlp.put("aBoolean", true); @@ -85,7 +119,6 @@ public abstract class Neo4jConversionsITBase { CYPHER_TYPES = Collections.unmodifiableMap(hlp); } - protected static final Map ADDITIONAL_TYPES; static { Map hlp = new HashMap<>(); hlp.put("booleanArray", new boolean[] { true, true, false }); @@ -115,8 +148,9 @@ public abstract class Neo4jConversionsITBase { hlp.put("aUUID", UUID.fromString("d4ec9208-4b17-4ec7-a709-19a5e53865a8")); try { hlp.put("aURL", new URL("https://www.test.com")); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + } + catch (MalformedURLException ex) { + throw new RuntimeException(ex); } hlp.put("aURI", URI.create("urn:isbn:9783864905254")); hlp.put("anEnum", SomeEnum.TheUsualMisfit); @@ -131,9 +165,120 @@ public abstract class Neo4jConversionsITBase { ADDITIONAL_TYPES = Collections.unmodifiableMap(hlp); } + static { + Map hlp = new HashMap<>(); + hlp.put("sdnPoint", NEO_HQ.asSpringPoint()); + hlp.put("geo2d", MINC.asGeo2d()); + hlp.put("geo3d", CLARION.asGeo3d(27.0)); + hlp.put("car2d", new CartesianPoint2d(10, 20)); + hlp.put("car3d", new CartesianPoint3d(30, 40, 50)); + SPATIAL_TYPES = Collections.unmodifiableMap(hlp); + } + + static { + Map hlp = new HashMap<>(); + hlp.put("customType", ThingWithCustomTypes.CustomType.of("ABCD")); + hlp.put("dateAsLong", + Date.from(ZonedDateTime.of(2020, 9, 21, 12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant())); + hlp.put("dateAsString", + Date.from(ZonedDateTime.of(2013, 5, 6, 12, 0, 0, 0, ZoneId.of("Europe/Berlin")) + .toInstant() + .truncatedTo(ChronoUnit.DAYS))); + CUSTOM_TYPES = Collections.unmodifiableMap(hlp); + } + + @BeforeAll + static void prepareData() { + + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { + session.executeWriteWithoutResult(w -> { + Map parameters; + + w.run("MATCH (n) detach delete n"); + + parameters = new HashMap<>(); + parameters.put("aByteArray", "A thing".getBytes()); + ID_OF_CYPHER_TYPES_NODE = w.run(""" + CREATE (n:CypherTypes) + SET + n.aBoolean = true, + n.aLong = 9223372036854775807, n.aDouble = 1.7976931348, n.aString = 'Hallo, Cypher', + n.aByteArray = $aByteArray, n.aLocalDate = date('2015-07-21'), + n.anOffsetTime = time({ hour:12, minute:31, timezone: '+01:00' }), + n.aLocalTime = localtime({ hour:12, minute:31, second:14 }), + n.aZoneDateTime = datetime('2015-07-21T21:40:32-04[America/New_York]'), + n.aLocalDateTime = localdatetime('2015202T21'), n.anIsoDuration = duration('P14DT16H12M'), + n.aPoint = point({x:47, y:11}) + RETURN id(n) AS id + """, parameters).single().get("id").asLong(); + + parameters = new HashMap<>(); + parameters.put("aByte", Values.value(new byte[] { 6 })); + ID_OF_ADDITIONAL_TYPES_NODE = w + .run(""" + CREATE (n:AdditionalTypes) + SET + n.booleanArray = [true, true, false], n.aByte = $aByte, + n.aChar = 'x', n.charArray = ['x', 'y', 'z'], n.aDate = '2019-09-21T13:23:11Z', + n.doubleArray = [1.1, 2.2, 3.3], n.aFloat = '23.42', n.floatArray = ['4.4', '5.5'], + n.anInt = 42, n.intArray = [21, 9], n.aLocale = 'de_DE', + n.longArray = [-9223372036854775808, 9223372036854775807], n.aShort = 127, + n.shortArray = [-10, 10], n.aBigDecimal = '1.79769313486231570E+309', + n.aBigInteger = '92233720368547758070', n.aPeriod = duration('P23Y4M7D'), + n.aDuration = duration('PT26H4M5S'), n.stringArray = ['Hallo', 'Welt'], + n.listOfStrings = ['Hello', 'World'], n.setOfStrings = ['Hallo', 'Welt'], + n.anInstant = datetime('2019-09-26T20:34:23Z'), + n.aUUID = 'd4ec9208-4b17-4ec7-a709-19a5e53865a8', n.listOfDoubles = [1.0], + n.aURL = 'https://www.test.com', + n.aURI = 'urn:isbn:9783864905254', + n.anEnum = 'TheUsualMisfit', n.anArrayOfEnums = ['ValueA', 'ValueB'], + n.aCollectionOfEnums = ['ValueC', 'TheUsualMisfit'], + n.aTimeZone = 'America/Los_Angeles',\s + n.aZoneId = 'America/New_York', n.aZeroPeriod = duration('PT0S'), n.aZeroDuration = duration('PT0S'), + n.aVector = [0.1, 0.2] + RETURN id(n) AS id + """, + parameters) + .single() + .get("id") + .asLong(); + + parameters = new HashMap<>(); + parameters.put("neo4j", NEO_HQ.toParameterMap()); + parameters.put("minc", MINC.toParameterMap()); + parameters.put("clarion", CLARION.toParameterMap()); + parameters.put("aByte", Values.value(new byte[] { 6 })); + ID_OF_SPATIAL_TYPES_NODE = w.run(""" + CREATE (n:SpatialTypes) + SET + n.sdnPoint = point({latitude: $neo4j.latitude, longitude: $neo4j.longitude}), + n.geo2d = point({latitude: $minc.latitude, longitude: $minc.longitude}), + n.geo3d = point({latitude: $clarion.latitude, longitude: $clarion.longitude, height: 27}), + n.car2d = point({x: 10, y: 20}), n.car3d = point({x: 30, y: 40, z: 50}) + RETURN id(n) AS id + """, parameters).single().get("id").asLong(); + + parameters = new HashMap<>(); + parameters.put("customType", "ABCD"); + parameters.put("dateAsLong", 1600682400000L); + parameters.put("dateAsString", "2013-05-06"); + ID_OF_CUSTOM_TYPE_NODE = w.run( + "CREATE (n:CustomTypes) SET n.customType = $customType, n.dateAsLong = $dateAsLong, n.dateAsString = $dateAsString RETURN id(n) AS id", + parameters) + .single() + .get("id") + .asLong(); + }); + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + protected static class ParamHolder { + String name; + double latitude; + double longitude; ParamHolder(String name, double latitude, double longitude) { @@ -155,21 +300,24 @@ public abstract class Neo4jConversionsITBase { } Point asSpringPoint() { - return new Point(latitude, longitude); + return new Point(this.latitude, this.longitude); } GeographicPoint2d asGeo2d() { - return new GeographicPoint2d(latitude, longitude); + return new GeographicPoint2d(this.latitude, this.longitude); } GeographicPoint3d asGeo3d(double height) { - return new GeographicPoint3d(latitude, longitude, height); + return new GeographicPoint3d(this.latitude, this.longitude, height); } @SuppressWarnings("HiddenField") public static class ParamHolderBuilder { + private String name; + private double latitude; + private double longitude; ParamHolderBuilder() { @@ -194,121 +342,14 @@ public abstract class Neo4jConversionsITBase { return new ParamHolder(this.name, this.latitude, this.longitude); } + @Override public String toString() { - return "Neo4jConversionsITBase.ParamHolder.ParamHolderBuilder(name=" + this.name + ", latitude=" + this.latitude + ", longitude=" + this.longitude + ")"; + return "Neo4jConversionsITBase.ParamHolder.ParamHolderBuilder(name=" + this.name + ", latitude=" + + this.latitude + ", longitude=" + this.longitude + ")"; } + } + } - private static final ParamHolder NEO_HQ = ParamHolder.builder().latitude(55.612191).longitude(12.994823) - .name("Neo4j HQ").build(); - private static final ParamHolder CLARION = ParamHolder.builder().latitude(55.607726).longitude(12.994243) - .name("Clarion").build(); - private static final ParamHolder MINC = ParamHolder.builder().latitude(55.611496).longitude(12.994039).name("Minc") - .build(); - - protected static final Map SPATIAL_TYPES; - static { - Map hlp = new HashMap<>(); - hlp.put("sdnPoint", NEO_HQ.asSpringPoint()); - hlp.put("geo2d", MINC.asGeo2d()); - hlp.put("geo3d", CLARION.asGeo3d(27.0)); - hlp.put("car2d", new CartesianPoint2d(10, 20)); - hlp.put("car3d", new CartesianPoint3d(30, 40, 50)); - SPATIAL_TYPES = Collections.unmodifiableMap(hlp); - } - - protected static final Map CUSTOM_TYPES; - static { - Map hlp = new HashMap<>(); - hlp.put("customType", ThingWithCustomTypes.CustomType.of("ABCD")); - hlp.put("dateAsLong", Date.from(ZonedDateTime.of(2020, 9, 21, - 12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant())); - hlp.put("dateAsString", Date.from(ZonedDateTime.of(2013, 5, 6, - 12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant().truncatedTo(ChronoUnit.DAYS))); - CUSTOM_TYPES = Collections.unmodifiableMap(hlp); - } - - protected static long ID_OF_CYPHER_TYPES_NODE; - protected static long ID_OF_ADDITIONAL_TYPES_NODE; - protected static long ID_OF_SPATIAL_TYPES_NODE; - protected static long ID_OF_CUSTOM_TYPE_NODE; - - @BeforeAll - static void prepareData() { - - try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { - session.executeWriteWithoutResult(w -> { - Map parameters; - - w.run("MATCH (n) detach delete n"); - - parameters = new HashMap<>(); - parameters.put("aByteArray", "A thing".getBytes()); - ID_OF_CYPHER_TYPES_NODE = w.run(""" - CREATE (n:CypherTypes) - SET - n.aBoolean = true, - n.aLong = 9223372036854775807, n.aDouble = 1.7976931348, n.aString = 'Hallo, Cypher', - n.aByteArray = $aByteArray, n.aLocalDate = date('2015-07-21'), - n.anOffsetTime = time({ hour:12, minute:31, timezone: '+01:00' }), - n.aLocalTime = localtime({ hour:12, minute:31, second:14 }), - n.aZoneDateTime = datetime('2015-07-21T21:40:32-04[America/New_York]'), - n.aLocalDateTime = localdatetime('2015202T21'), n.anIsoDuration = duration('P14DT16H12M'), - n.aPoint = point({x:47, y:11}) - RETURN id(n) AS id - """, parameters).single().get("id").asLong(); - - parameters = new HashMap<>(); - parameters.put("aByte", Values.value(new byte[] { 6 })); - ID_OF_ADDITIONAL_TYPES_NODE = w.run(""" - CREATE (n:AdditionalTypes) - SET - n.booleanArray = [true, true, false], n.aByte = $aByte, - n.aChar = 'x', n.charArray = ['x', 'y', 'z'], n.aDate = '2019-09-21T13:23:11Z', - n.doubleArray = [1.1, 2.2, 3.3], n.aFloat = '23.42', n.floatArray = ['4.4', '5.5'], - n.anInt = 42, n.intArray = [21, 9], n.aLocale = 'de_DE', - n.longArray = [-9223372036854775808, 9223372036854775807], n.aShort = 127, - n.shortArray = [-10, 10], n.aBigDecimal = '1.79769313486231570E+309', - n.aBigInteger = '92233720368547758070', n.aPeriod = duration('P23Y4M7D'), - n.aDuration = duration('PT26H4M5S'), n.stringArray = ['Hallo', 'Welt'], - n.listOfStrings = ['Hello', 'World'], n.setOfStrings = ['Hallo', 'Welt'], - n.anInstant = datetime('2019-09-26T20:34:23Z'), - n.aUUID = 'd4ec9208-4b17-4ec7-a709-19a5e53865a8', n.listOfDoubles = [1.0], - n.aURL = 'https://www.test.com', - n.aURI = 'urn:isbn:9783864905254', - n.anEnum = 'TheUsualMisfit', n.anArrayOfEnums = ['ValueA', 'ValueB'], - n.aCollectionOfEnums = ['ValueC', 'TheUsualMisfit'], - n.aTimeZone = 'America/Los_Angeles',\s - n.aZoneId = 'America/New_York', n.aZeroPeriod = duration('PT0S'), n.aZeroDuration = duration('PT0S'), - n.aVector = [0.1, 0.2] - RETURN id(n) AS id - """, parameters).single().get("id").asLong(); - - parameters = new HashMap<>(); - parameters.put("neo4j", NEO_HQ.toParameterMap()); - parameters.put("minc", MINC.toParameterMap()); - parameters.put("clarion", CLARION.toParameterMap()); - parameters.put("aByte", Values.value(new byte[] { 6 })); - ID_OF_SPATIAL_TYPES_NODE = w.run(""" - CREATE (n:SpatialTypes) - SET - n.sdnPoint = point({latitude: $neo4j.latitude, longitude: $neo4j.longitude}), - n.geo2d = point({latitude: $minc.latitude, longitude: $minc.longitude}), - n.geo3d = point({latitude: $clarion.latitude, longitude: $clarion.longitude, height: 27}), - n.car2d = point({x: 10, y: 20}), n.car3d = point({x: 30, y: 40, z: 50}) - RETURN id(n) AS id - """, parameters).single().get("id").asLong(); - - parameters = new HashMap<>(); - parameters.put("customType", "ABCD"); - parameters.put("dateAsLong", 1600682400000L); - parameters.put("dateAsString", "2013-05-06"); - ID_OF_CUSTOM_TYPE_NODE = w - .run("CREATE (n:CustomTypes) SET n.customType = $customType, n.dateAsLong = $dateAsLong, n.dateAsString = $dateAsString RETURN id(n) AS id", parameters) - .single().get("id").asLong(); - }); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/PersonWithCustomId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/PersonWithCustomId.java index de6fb7366..9f15e5bdc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/PersonWithCustomId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/PersonWithCustomId.java @@ -15,16 +15,18 @@ */ package org.springframework.data.neo4j.integration.shared.conversion; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + import org.neo4j.driver.Values; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - /** * @author Rosetta Roberts * @author Michael J. Simons @@ -32,6 +34,11 @@ import java.util.Set; @Node public final class PersonWithCustomId { + @Id + private final PersonId id; + + private final String name; + public PersonWithCustomId(PersonId id, String name) { this.id = id; this.name = name; @@ -45,6 +52,7 @@ public final class PersonWithCustomId { return this.name; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -55,27 +63,26 @@ public final class PersonWithCustomId { final PersonWithCustomId other = (PersonWithCustomId) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + if (!Objects.equals(this$id, other$id)) { return false; } final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) { - return false; - } - return true; + return Objects.equals(this$name, other$name); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); final Object $name = this.getName(); - result = result * PRIME + ($name == null ? 43 : $name.hashCode()); + result = (result * PRIME) + (($name != null) ? $name.hashCode() : 43); return result; } + @Override public String toString() { return "PersonWithCustomId(id=" + this.getId() + ", name=" + this.getName() + ")"; } @@ -98,6 +105,7 @@ public final class PersonWithCustomId { return this.id; } + @Override public boolean equals(final Object o) { if (o == this) { return true; @@ -108,36 +116,35 @@ public final class PersonWithCustomId { final PersonId other = (PersonId) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - return true; + return Objects.equals(this$id, other$id); } + @Override public int hashCode() { final int PRIME = 59; int result = 1; final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); return result; } + @Override public String toString() { return "PersonWithCustomId.PersonId(id=" + this.getId() + ")"; } + } /** - * Converted needed to deal with the above custom type. Without that converter, an association would be assumed. + * Converted needed to deal with the above custom type. Without that converter, an + * association would be assumed. */ public static class CustomPersonIdConverter implements GenericConverter { @Override public Set getConvertibleTypes() { - return new HashSet<>(Arrays.asList( - new ConvertiblePair(PersonId.class, org.neo4j.driver.Value.class), - new ConvertiblePair(org.neo4j.driver.Value.class, PersonId.class) - )); + return new HashSet<>(Arrays.asList(new ConvertiblePair(PersonId.class, org.neo4j.driver.Value.class), + new ConvertiblePair(org.neo4j.driver.Value.class, PersonId.class))); } @Override @@ -148,14 +155,12 @@ public final class PersonWithCustomId { if (PersonId.class.isAssignableFrom(type1.getType())) { return Values.value(((PersonId) o).getId()); - } else { + } + else { return new PersonId(((org.neo4j.driver.Value) o).asLong()); } } + } - @Id - private final PersonId id; - - private final String name; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/RelationshipWithCompositeProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/RelationshipWithCompositeProperties.java index 919f02fb9..1f025669b 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/RelationshipWithCompositeProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/RelationshipWithCompositeProperties.java @@ -27,7 +27,6 @@ import org.springframework.data.neo4j.integration.shared.common.Club; * Just a holder for composite properties on relationships. * * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Learning English, Lesson Two */ @RelationshipProperties public class RelationshipWithCompositeProperties { @@ -49,11 +48,11 @@ public class RelationshipWithCompositeProperties { } public Club getOtherThing() { - return otherThing; + return this.otherThing; } public Map getSomeProperties() { - return someProperties; + return this.someProperties; } public void setSomeProperties(Map someProperties) { @@ -61,11 +60,11 @@ public class RelationshipWithCompositeProperties { } public ThingWithCompositeProperties.SomeOtherDTO getSomeOtherDTO() { - return someOtherDTO; + return this.someOtherDTO; } - public void setSomeOtherDTO( - ThingWithCompositeProperties.SomeOtherDTO someOtherDTO) { + public void setSomeOtherDTO(ThingWithCompositeProperties.SomeOtherDTO someOtherDTO) { this.someOtherDTO = someOtherDTO; } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java index 1a5f4de08..e9096516c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java @@ -43,565 +43,7 @@ import org.springframework.data.neo4j.core.schema.Node; */ @SuppressWarnings("HiddenField") @Node("AdditionalTypes") -public class ThingWithAllAdditionalTypes { - - private ThingWithAllAdditionalTypes(Long id, boolean[] booleanArray, byte aByte, char aChar, char[] charArray, Date aDate, BigDecimal aBigDecimal, BigInteger aBigInteger, double[] doubleArray, float aFloat, float[] floatArray, int anInt, int[] intArray, Locale aLocale, long[] longArray, short aShort, short[] shortArray, Period aPeriod, Duration aDuration, String[] stringArray, List listOfStrings, Set setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, SomeEnum[] anArrayOfEnums, List listOfDoubles, List aCollectionOfEnums, TimeZone aTimeZone, ZoneId aZoneId, Period aZeroPeriod, Duration aZeroDuration, Vector aVector) { - this.id = id; - this.booleanArray = booleanArray; - this.aByte = aByte; - this.aChar = aChar; - this.charArray = charArray; - this.aDate = aDate; - this.aBigDecimal = aBigDecimal; - this.aBigInteger = aBigInteger; - this.doubleArray = doubleArray; - this.aFloat = aFloat; - this.floatArray = floatArray; - this.anInt = anInt; - this.intArray = intArray; - this.aLocale = aLocale; - this.longArray = longArray; - this.aShort = aShort; - this.shortArray = shortArray; - this.aPeriod = aPeriod; - this.aDuration = aDuration; - this.stringArray = stringArray; - this.listOfStrings = listOfStrings; - this.setOfStrings = setOfStrings; - this.anInstant = anInstant; - this.aUUID = aUUID; - this.aURL = aURL; - this.aURI = aURI; - this.anEnum = anEnum; - this.anArrayOfEnums = anArrayOfEnums; - this.listOfDoubles = listOfDoubles; - this.aCollectionOfEnums = aCollectionOfEnums; - this.aTimeZone = aTimeZone; - this.aZoneId = aZoneId; - this.aZeroPeriod = aZeroPeriod; - this.aZeroDuration = aZeroDuration; - this.aVector = aVector; - } - - public static ThingWithAllAdditionalTypesBuilder builder() { - return new ThingWithAllAdditionalTypesBuilder(); - } - - public Long getId() { - return this.id; - } - - public boolean[] getBooleanArray() { - return this.booleanArray; - } - - public byte getAByte() { - return this.aByte; - } - - public char getAChar() { - return this.aChar; - } - - public char[] getCharArray() { - return this.charArray; - } - - public Date getADate() { - return this.aDate; - } - - public BigDecimal getABigDecimal() { - return this.aBigDecimal; - } - - public BigInteger getABigInteger() { - return this.aBigInteger; - } - - public double[] getDoubleArray() { - return this.doubleArray; - } - - public float getAFloat() { - return this.aFloat; - } - - public float[] getFloatArray() { - return this.floatArray; - } - - public int getAnInt() { - return this.anInt; - } - - public int[] getIntArray() { - return this.intArray; - } - - public Locale getALocale() { - return this.aLocale; - } - - public long[] getLongArray() { - return this.longArray; - } - - public short getAShort() { - return this.aShort; - } - - public short[] getShortArray() { - return this.shortArray; - } - - public Period getAPeriod() { - return this.aPeriod; - } - - public Duration getADuration() { - return this.aDuration; - } - - public String[] getStringArray() { - return this.stringArray; - } - - public List getListOfStrings() { - return this.listOfStrings; - } - - public Set getSetOfStrings() { - return this.setOfStrings; - } - - public Instant getAnInstant() { - return this.anInstant; - } - - public UUID getAUUID() { - return this.aUUID; - } - - public URL getAURL() { - return this.aURL; - } - - public URI getAURI() { - return this.aURI; - } - - public SomeEnum getAnEnum() { - return this.anEnum; - } - - public SomeEnum[] getAnArrayOfEnums() { - return this.anArrayOfEnums; - } - - public List getListOfDoubles() { - return this.listOfDoubles; - } - - public List getACollectionOfEnums() { - return this.aCollectionOfEnums; - } - - public TimeZone getATimeZone() { - return this.aTimeZone; - } - - public ZoneId getAZoneId() { - return this.aZoneId; - } - - public Period getAZeroPeriod() { - return this.aZeroPeriod; - } - - public Duration getAZeroDuration() { - return this.aZeroDuration; - } - - public Vector getAVector() { - return this.aVector; - } - - public void setBooleanArray(boolean[] booleanArray) { - this.booleanArray = booleanArray; - } - - public void setAByte(byte aByte) { - this.aByte = aByte; - } - - public void setAChar(char aChar) { - this.aChar = aChar; - } - - public void setCharArray(char[] charArray) { - this.charArray = charArray; - } - - public void setADate(Date aDate) { - this.aDate = aDate; - } - - public void setABigDecimal(BigDecimal aBigDecimal) { - this.aBigDecimal = aBigDecimal; - } - - public void setABigInteger(BigInteger aBigInteger) { - this.aBigInteger = aBigInteger; - } - - public void setDoubleArray(double[] doubleArray) { - this.doubleArray = doubleArray; - } - - public void setAFloat(float aFloat) { - this.aFloat = aFloat; - } - - public void setFloatArray(float[] floatArray) { - this.floatArray = floatArray; - } - - public void setAnInt(int anInt) { - this.anInt = anInt; - } - - public void setIntArray(int[] intArray) { - this.intArray = intArray; - } - - public void setALocale(Locale aLocale) { - this.aLocale = aLocale; - } - - public void setLongArray(long[] longArray) { - this.longArray = longArray; - } - - public void setAShort(short aShort) { - this.aShort = aShort; - } - - public void setShortArray(short[] shortArray) { - this.shortArray = shortArray; - } - - public void setAPeriod(Period aPeriod) { - this.aPeriod = aPeriod; - } - - public void setADuration(Duration aDuration) { - this.aDuration = aDuration; - } - - public void setStringArray(String[] stringArray) { - this.stringArray = stringArray; - } - - public void setListOfStrings(List listOfStrings) { - this.listOfStrings = listOfStrings; - } - - public void setSetOfStrings(Set setOfStrings) { - this.setOfStrings = setOfStrings; - } - - public void setAnInstant(Instant anInstant) { - this.anInstant = anInstant; - } - - public void setAUUID(UUID aUUID) { - this.aUUID = aUUID; - } - - public void setAURL(URL aURL) { - this.aURL = aURL; - } - - public void setAURI(URI aURI) { - this.aURI = aURI; - } - - public void setAnEnum(SomeEnum anEnum) { - this.anEnum = anEnum; - } - - public void setAnArrayOfEnums(SomeEnum[] anArrayOfEnums) { - this.anArrayOfEnums = anArrayOfEnums; - } - - public void setListOfDoubles(List listOfDoubles) { - this.listOfDoubles = listOfDoubles; - } - - public void setACollectionOfEnums(List aCollectionOfEnums) { - this.aCollectionOfEnums = aCollectionOfEnums; - } - - public void setATimeZone(TimeZone aTimeZone) { - this.aTimeZone = aTimeZone; - } - - public void setAZoneId(ZoneId aZoneId) { - this.aZoneId = aZoneId; - } - - public void setAZeroPeriod(Period aZeroPeriod) { - this.aZeroPeriod = aZeroPeriod; - } - - public void setAZeroDuration(Duration aZeroDuration) { - this.aZeroDuration = aZeroDuration; - } - - public void setAVector(Vector aVector) { - this.aVector = aVector; - } - - public boolean equals(final Object o) { - if (o == this) { - return true; - } - if (!(o instanceof ThingWithAllAdditionalTypes)) { - return false; - } - final ThingWithAllAdditionalTypes other = (ThingWithAllAdditionalTypes) o; - if (!other.canEqual((Object) this)) { - return false; - } - final Object this$id = this.getId(); - final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) { - return false; - } - if (!java.util.Arrays.equals(this.getBooleanArray(), other.getBooleanArray())) { - return false; - } - if (this.getAByte() != other.getAByte()) { - return false; - } - if (this.getAChar() != other.getAChar()) { - return false; - } - if (!java.util.Arrays.equals(this.getCharArray(), other.getCharArray())) { - return false; - } - final Object this$aDate = this.getADate(); - final Object other$aDate = other.getADate(); - if (this$aDate == null ? other$aDate != null : !this$aDate.equals(other$aDate)) { - return false; - } - final Object this$aBigDecimal = this.getABigDecimal(); - final Object other$aBigDecimal = other.getABigDecimal(); - if (this$aBigDecimal == null ? other$aBigDecimal != null : !this$aBigDecimal.equals(other$aBigDecimal)) { - return false; - } - final Object this$aBigInteger = this.getABigInteger(); - final Object other$aBigInteger = other.getABigInteger(); - if (this$aBigInteger == null ? other$aBigInteger != null : !this$aBigInteger.equals(other$aBigInteger)) { - return false; - } - if (!java.util.Arrays.equals(this.getDoubleArray(), other.getDoubleArray())) { - return false; - } - if (Float.compare(this.getAFloat(), other.getAFloat()) != 0) { - return false; - } - if (!java.util.Arrays.equals(this.getFloatArray(), other.getFloatArray())) { - return false; - } - if (this.getAnInt() != other.getAnInt()) { - return false; - } - if (!java.util.Arrays.equals(this.getIntArray(), other.getIntArray())) { - return false; - } - final Object this$aLocale = this.getALocale(); - final Object other$aLocale = other.getALocale(); - if (this$aLocale == null ? other$aLocale != null : !this$aLocale.equals(other$aLocale)) { - return false; - } - if (!java.util.Arrays.equals(this.getLongArray(), other.getLongArray())) { - return false; - } - if (this.getAShort() != other.getAShort()) { - return false; - } - if (!java.util.Arrays.equals(this.getShortArray(), other.getShortArray())) { - return false; - } - final Object this$aPeriod = this.getAPeriod(); - final Object other$aPeriod = other.getAPeriod(); - if (this$aPeriod == null ? other$aPeriod != null : !this$aPeriod.equals(other$aPeriod)) { - return false; - } - final Object this$aDuration = this.getADuration(); - final Object other$aDuration = other.getADuration(); - if (this$aDuration == null ? other$aDuration != null : !this$aDuration.equals(other$aDuration)) { - return false; - } - if (!java.util.Arrays.deepEquals(this.getStringArray(), other.getStringArray())) { - return false; - } - final Object this$listOfStrings = this.getListOfStrings(); - final Object other$listOfStrings = other.getListOfStrings(); - if (this$listOfStrings == null ? other$listOfStrings != null : !this$listOfStrings.equals(other$listOfStrings)) { - return false; - } - final Object this$setOfStrings = this.getSetOfStrings(); - final Object other$setOfStrings = other.getSetOfStrings(); - if (this$setOfStrings == null ? other$setOfStrings != null : !this$setOfStrings.equals(other$setOfStrings)) { - return false; - } - final Object this$anInstant = this.getAnInstant(); - final Object other$anInstant = other.getAnInstant(); - if (this$anInstant == null ? other$anInstant != null : !this$anInstant.equals(other$anInstant)) { - return false; - } - final Object this$aUUID = this.getAUUID(); - final Object other$aUUID = other.getAUUID(); - if (this$aUUID == null ? other$aUUID != null : !this$aUUID.equals(other$aUUID)) { - return false; - } - final Object this$aURL = this.getAURL(); - final Object other$aURL = other.getAURL(); - if (this$aURL == null ? other$aURL != null : !this$aURL.equals(other$aURL)) { - return false; - } - final Object this$aURI = this.getAURI(); - final Object other$aURI = other.getAURI(); - if (this$aURI == null ? other$aURI != null : !this$aURI.equals(other$aURI)) { - return false; - } - final Object this$anEnum = this.getAnEnum(); - final Object other$anEnum = other.getAnEnum(); - if (this$anEnum == null ? other$anEnum != null : !this$anEnum.equals(other$anEnum)) { - return false; - } - if (!java.util.Arrays.deepEquals(this.getAnArrayOfEnums(), other.getAnArrayOfEnums())) { - return false; - } - final Object this$listOfDoubles = this.getListOfDoubles(); - final Object other$listOfDoubles = other.getListOfDoubles(); - if (this$listOfDoubles == null ? other$listOfDoubles != null : !this$listOfDoubles.equals(other$listOfDoubles)) { - return false; - } - final Object this$aCollectionOfEnums = this.getACollectionOfEnums(); - final Object other$aCollectionOfEnums = other.getACollectionOfEnums(); - if (this$aCollectionOfEnums == null ? other$aCollectionOfEnums != null : !this$aCollectionOfEnums.equals(other$aCollectionOfEnums)) { - return false; - } - final Object this$aTimeZone = this.getATimeZone(); - final Object other$aTimeZone = other.getATimeZone(); - if (this$aTimeZone == null ? other$aTimeZone != null : !this$aTimeZone.equals(other$aTimeZone)) { - return false; - } - final Object this$aZoneId = this.getAZoneId(); - final Object other$aZoneId = other.getAZoneId(); - if (this$aZoneId == null ? other$aZoneId != null : !this$aZoneId.equals(other$aZoneId)) { - return false; - } - final Object this$aZeroPeriod = this.getAZeroPeriod(); - final Object other$aZeroPeriod = other.getAZeroPeriod(); - if (this$aZeroPeriod == null ? other$aZeroPeriod != null : !this$aZeroPeriod.equals(other$aZeroPeriod)) { - return false; - } - final Object this$aZeroDuration = this.getAZeroDuration(); - final Object other$aZeroDuration = other.getAZeroDuration(); - if (this$aZeroDuration == null ? other$aZeroDuration != null : !this$aZeroDuration.equals(other$aZeroDuration)) { - return false; - } - - final Object this$aVector = this.getAVector(); - final Object other$aVector = other.getAVector(); - if (this$aVector == null ? other$aVector != null : !this$aVector.equals(other$aVector)) { - return false; - } - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof ThingWithAllAdditionalTypes; - } - - public int hashCode() { - final int PRIME = 59; - int result = 1; - final Object $id = this.getId(); - result = result * PRIME + ($id == null ? 43 : $id.hashCode()); - result = result * PRIME + java.util.Arrays.hashCode(this.getBooleanArray()); - result = result * PRIME + this.getAByte(); - result = result * PRIME + this.getAChar(); - result = result * PRIME + java.util.Arrays.hashCode(this.getCharArray()); - final Object $aDate = this.getADate(); - result = result * PRIME + ($aDate == null ? 43 : $aDate.hashCode()); - final Object $aBigDecimal = this.getABigDecimal(); - result = result * PRIME + ($aBigDecimal == null ? 43 : $aBigDecimal.hashCode()); - final Object $aBigInteger = this.getABigInteger(); - result = result * PRIME + ($aBigInteger == null ? 43 : $aBigInteger.hashCode()); - result = result * PRIME + java.util.Arrays.hashCode(this.getDoubleArray()); - result = result * PRIME + Float.floatToIntBits(this.getAFloat()); - result = result * PRIME + java.util.Arrays.hashCode(this.getFloatArray()); - result = result * PRIME + this.getAnInt(); - result = result * PRIME + java.util.Arrays.hashCode(this.getIntArray()); - final Object $aLocale = this.getALocale(); - result = result * PRIME + ($aLocale == null ? 43 : $aLocale.hashCode()); - result = result * PRIME + java.util.Arrays.hashCode(this.getLongArray()); - result = result * PRIME + this.getAShort(); - result = result * PRIME + java.util.Arrays.hashCode(this.getShortArray()); - final Object $aPeriod = this.getAPeriod(); - result = result * PRIME + ($aPeriod == null ? 43 : $aPeriod.hashCode()); - final Object $aDuration = this.getADuration(); - result = result * PRIME + ($aDuration == null ? 43 : $aDuration.hashCode()); - result = result * PRIME + java.util.Arrays.deepHashCode(this.getStringArray()); - final Object $listOfStrings = this.getListOfStrings(); - result = result * PRIME + ($listOfStrings == null ? 43 : $listOfStrings.hashCode()); - final Object $setOfStrings = this.getSetOfStrings(); - result = result * PRIME + ($setOfStrings == null ? 43 : $setOfStrings.hashCode()); - final Object $anInstant = this.getAnInstant(); - result = result * PRIME + ($anInstant == null ? 43 : $anInstant.hashCode()); - final Object $aUUID = this.getAUUID(); - result = result * PRIME + ($aUUID == null ? 43 : $aUUID.hashCode()); - final Object $aURL = this.getAURL(); - result = result * PRIME + ($aURL == null ? 43 : $aURL.hashCode()); - final Object $aURI = this.getAURI(); - result = result * PRIME + ($aURI == null ? 43 : $aURI.hashCode()); - final Object $anEnum = this.getAnEnum(); - result = result * PRIME + ($anEnum == null ? 43 : $anEnum.hashCode()); - result = result * PRIME + java.util.Arrays.deepHashCode(this.getAnArrayOfEnums()); - final Object $listOfDoubles = this.getListOfDoubles(); - result = result * PRIME + ($listOfDoubles == null ? 43 : $listOfDoubles.hashCode()); - final Object $aCollectionOfEnums = this.getACollectionOfEnums(); - result = result * PRIME + ($aCollectionOfEnums == null ? 43 : $aCollectionOfEnums.hashCode()); - final Object $aTimeZone = this.getATimeZone(); - result = result * PRIME + ($aTimeZone == null ? 43 : $aTimeZone.hashCode()); - final Object $aZoneId = this.getAZoneId(); - result = result * PRIME + ($aZoneId == null ? 43 : $aZoneId.hashCode()); - final Object $aZeroPeriod = this.getAZeroPeriod(); - result = result * PRIME + ($aZeroPeriod == null ? 43 : $aZeroPeriod.hashCode()); - final Object $aZeroDuration = this.getAZeroDuration(); - result = result * PRIME + ($aZeroDuration == null ? 43 : $aZeroDuration.hashCode()); - final Object $aVector = this.getAVector(); - result = result * PRIME + ($aVector == null ? 43 : $aVector.hashCode()); - return result; - } - - public String toString() { - return "ThingWithAllAdditionalTypes(id=" + this.getId() + ", booleanArray=" + java.util.Arrays.toString(this.getBooleanArray()) + ", aByte=" + this.getAByte() + ", aChar=" + this.getAChar() + ", charArray=" + java.util.Arrays.toString(this.getCharArray()) + ", aDate=" + this.getADate() + ", aBigDecimal=" + this.getABigDecimal() + ", aBigInteger=" + this.getABigInteger() + ", doubleArray=" + java.util.Arrays.toString(this.getDoubleArray()) + ", aFloat=" + this.getAFloat() + ", floatArray=" + java.util.Arrays.toString(this.getFloatArray()) + ", anInt=" + this.getAnInt() + ", intArray=" + java.util.Arrays.toString(this.getIntArray()) + ", aLocale=" + this.getALocale() + ", longArray=" + java.util.Arrays.toString(this.getLongArray()) + ", aShort=" + this.getAShort() + ", shortArray=" + java.util.Arrays.toString(this.getShortArray()) + ", aPeriod=" + this.getAPeriod() + ", aDuration=" + this.getADuration() + ", stringArray=" + java.util.Arrays.deepToString(this.getStringArray()) + ", listOfStrings=" + this.getListOfStrings() + ", setOfStrings=" + this.getSetOfStrings() + ", anInstant=" + this.getAnInstant() + ", aUUID=" + this.getAUUID() + ", aURL=" + this.getAURL() + ", aURI=" + this.getAURI() + ", anEnum=" + this.getAnEnum() + ", anArrayOfEnums=" + java.util.Arrays.deepToString(this.getAnArrayOfEnums()) + ", listOfDoubles=" + this.getListOfDoubles() + ", aCollectionOfEnums=" + this.getACollectionOfEnums() + ", aTimeZone=" + this.getATimeZone() + ", aZoneId=" + this.getAZoneId() + ", aZeroPeriod=" + this.getAZeroPeriod() + ", aZeroDuration=" + this.getAZeroDuration() + ")"; - } - - public ThingWithAllAdditionalTypes withId(Long id) { - return Objects.equals(this.id, id) ? this : new ThingWithAllAdditionalTypes(id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration, this.aVector); - } - - enum SomeEnum { - ValueA, ValueB, TheUsualMisfit, ValueC - } +public final class ThingWithAllAdditionalTypes { @Id @GeneratedValue @@ -675,44 +117,668 @@ public class ThingWithAllAdditionalTypes { private Vector aVector; + private ThingWithAllAdditionalTypes(Long id, boolean[] booleanArray, byte aByte, char aChar, char[] charArray, + Date aDate, BigDecimal aBigDecimal, BigInteger aBigInteger, double[] doubleArray, float aFloat, + float[] floatArray, int anInt, int[] intArray, Locale aLocale, long[] longArray, short aShort, + short[] shortArray, Period aPeriod, Duration aDuration, String[] stringArray, List listOfStrings, + Set setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, + SomeEnum[] anArrayOfEnums, List listOfDoubles, List aCollectionOfEnums, + TimeZone aTimeZone, ZoneId aZoneId, Period aZeroPeriod, Duration aZeroDuration, Vector aVector) { + this.id = id; + this.booleanArray = booleanArray; + this.aByte = aByte; + this.aChar = aChar; + this.charArray = charArray; + this.aDate = aDate; + this.aBigDecimal = aBigDecimal; + this.aBigInteger = aBigInteger; + this.doubleArray = doubleArray; + this.aFloat = aFloat; + this.floatArray = floatArray; + this.anInt = anInt; + this.intArray = intArray; + this.aLocale = aLocale; + this.longArray = longArray; + this.aShort = aShort; + this.shortArray = shortArray; + this.aPeriod = aPeriod; + this.aDuration = aDuration; + this.stringArray = stringArray; + this.listOfStrings = listOfStrings; + this.setOfStrings = setOfStrings; + this.anInstant = anInstant; + this.aUUID = aUUID; + this.aURL = aURL; + this.aURI = aURI; + this.anEnum = anEnum; + this.anArrayOfEnums = anArrayOfEnums; + this.listOfDoubles = listOfDoubles; + this.aCollectionOfEnums = aCollectionOfEnums; + this.aTimeZone = aTimeZone; + this.aZoneId = aZoneId; + this.aZeroPeriod = aZeroPeriod; + this.aZeroDuration = aZeroDuration; + this.aVector = aVector; + } + + public static ThingWithAllAdditionalTypesBuilder builder() { + return new ThingWithAllAdditionalTypesBuilder(); + } + + public Long getId() { + return this.id; + } + + public boolean[] getBooleanArray() { + return this.booleanArray; + } + + public void setBooleanArray(boolean[] booleanArray) { + this.booleanArray = booleanArray; + } + + public byte getAByte() { + return this.aByte; + } + + public void setAByte(byte aByte) { + this.aByte = aByte; + } + + public char getAChar() { + return this.aChar; + } + + public void setAChar(char aChar) { + this.aChar = aChar; + } + + public char[] getCharArray() { + return this.charArray; + } + + public void setCharArray(char[] charArray) { + this.charArray = charArray; + } + + public Date getADate() { + return this.aDate; + } + + public void setADate(Date aDate) { + this.aDate = aDate; + } + + public BigDecimal getABigDecimal() { + return this.aBigDecimal; + } + + public void setABigDecimal(BigDecimal aBigDecimal) { + this.aBigDecimal = aBigDecimal; + } + + public BigInteger getABigInteger() { + return this.aBigInteger; + } + + public void setABigInteger(BigInteger aBigInteger) { + this.aBigInteger = aBigInteger; + } + + public double[] getDoubleArray() { + return this.doubleArray; + } + + public void setDoubleArray(double[] doubleArray) { + this.doubleArray = doubleArray; + } + + public float getAFloat() { + return this.aFloat; + } + + public void setAFloat(float aFloat) { + this.aFloat = aFloat; + } + + public float[] getFloatArray() { + return this.floatArray; + } + + public void setFloatArray(float[] floatArray) { + this.floatArray = floatArray; + } + + public int getAnInt() { + return this.anInt; + } + + public void setAnInt(int anInt) { + this.anInt = anInt; + } + + public int[] getIntArray() { + return this.intArray; + } + + public void setIntArray(int[] intArray) { + this.intArray = intArray; + } + + public Locale getALocale() { + return this.aLocale; + } + + public void setALocale(Locale aLocale) { + this.aLocale = aLocale; + } + + public long[] getLongArray() { + return this.longArray; + } + + public void setLongArray(long[] longArray) { + this.longArray = longArray; + } + + public short getAShort() { + return this.aShort; + } + + public void setAShort(short aShort) { + this.aShort = aShort; + } + + public short[] getShortArray() { + return this.shortArray; + } + + public void setShortArray(short[] shortArray) { + this.shortArray = shortArray; + } + + public Period getAPeriod() { + return this.aPeriod; + } + + public void setAPeriod(Period aPeriod) { + this.aPeriod = aPeriod; + } + + public Duration getADuration() { + return this.aDuration; + } + + public void setADuration(Duration aDuration) { + this.aDuration = aDuration; + } + + public String[] getStringArray() { + return this.stringArray; + } + + public void setStringArray(String[] stringArray) { + this.stringArray = stringArray; + } + + public List getListOfStrings() { + return this.listOfStrings; + } + + public void setListOfStrings(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + public Set getSetOfStrings() { + return this.setOfStrings; + } + + public void setSetOfStrings(Set setOfStrings) { + this.setOfStrings = setOfStrings; + } + + public Instant getAnInstant() { + return this.anInstant; + } + + public void setAnInstant(Instant anInstant) { + this.anInstant = anInstant; + } + + public UUID getAUUID() { + return this.aUUID; + } + + public void setAUUID(UUID aUUID) { + this.aUUID = aUUID; + } + + public URL getAURL() { + return this.aURL; + } + + public void setAURL(URL aURL) { + this.aURL = aURL; + } + + public URI getAURI() { + return this.aURI; + } + + public void setAURI(URI aURI) { + this.aURI = aURI; + } + + public SomeEnum getAnEnum() { + return this.anEnum; + } + + public void setAnEnum(SomeEnum anEnum) { + this.anEnum = anEnum; + } + + public SomeEnum[] getAnArrayOfEnums() { + return this.anArrayOfEnums; + } + + public void setAnArrayOfEnums(SomeEnum[] anArrayOfEnums) { + this.anArrayOfEnums = anArrayOfEnums; + } + + public List getListOfDoubles() { + return this.listOfDoubles; + } + + public void setListOfDoubles(List listOfDoubles) { + this.listOfDoubles = listOfDoubles; + } + + public List getACollectionOfEnums() { + return this.aCollectionOfEnums; + } + + public void setACollectionOfEnums(List aCollectionOfEnums) { + this.aCollectionOfEnums = aCollectionOfEnums; + } + + public TimeZone getATimeZone() { + return this.aTimeZone; + } + + public void setATimeZone(TimeZone aTimeZone) { + this.aTimeZone = aTimeZone; + } + + public ZoneId getAZoneId() { + return this.aZoneId; + } + + public void setAZoneId(ZoneId aZoneId) { + this.aZoneId = aZoneId; + } + + public Period getAZeroPeriod() { + return this.aZeroPeriod; + } + + public void setAZeroPeriod(Period aZeroPeriod) { + this.aZeroPeriod = aZeroPeriod; + } + + public Duration getAZeroDuration() { + return this.aZeroDuration; + } + + public void setAZeroDuration(Duration aZeroDuration) { + this.aZeroDuration = aZeroDuration; + } + + public Vector getAVector() { + return this.aVector; + } + + public void setAVector(Vector aVector) { + this.aVector = aVector; + } + + protected boolean canEqual(final Object other) { + return other instanceof ThingWithAllAdditionalTypes; + } + + public ThingWithAllAdditionalTypes withId(Long id) { + return Objects.equals(this.id, id) ? this + : new ThingWithAllAdditionalTypes(id, this.booleanArray, this.aByte, this.aChar, this.charArray, + this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, + this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, + this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, + this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, + this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, + this.aZeroDuration, this.aVector); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ThingWithAllAdditionalTypes)) { + return false; + } + final ThingWithAllAdditionalTypes other = (ThingWithAllAdditionalTypes) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$id = this.getId(); + final Object other$id = other.getId(); + if (!Objects.equals(this$id, other$id)) { + return false; + } + if (!java.util.Arrays.equals(this.getBooleanArray(), other.getBooleanArray())) { + return false; + } + if (this.getAByte() != other.getAByte()) { + return false; + } + if (this.getAChar() != other.getAChar()) { + return false; + } + if (!java.util.Arrays.equals(this.getCharArray(), other.getCharArray())) { + return false; + } + final Object this$aDate = this.getADate(); + final Object other$aDate = other.getADate(); + if (!Objects.equals(this$aDate, other$aDate)) { + return false; + } + final Object this$aBigDecimal = this.getABigDecimal(); + final Object other$aBigDecimal = other.getABigDecimal(); + if (!Objects.equals(this$aBigDecimal, other$aBigDecimal)) { + return false; + } + final Object this$aBigInteger = this.getABigInteger(); + final Object other$aBigInteger = other.getABigInteger(); + if (!Objects.equals(this$aBigInteger, other$aBigInteger)) { + return false; + } + if (!java.util.Arrays.equals(this.getDoubleArray(), other.getDoubleArray())) { + return false; + } + if (Float.compare(this.getAFloat(), other.getAFloat()) != 0) { + return false; + } + if (!java.util.Arrays.equals(this.getFloatArray(), other.getFloatArray())) { + return false; + } + if (this.getAnInt() != other.getAnInt()) { + return false; + } + if (!java.util.Arrays.equals(this.getIntArray(), other.getIntArray())) { + return false; + } + final Object this$aLocale = this.getALocale(); + final Object other$aLocale = other.getALocale(); + if (!Objects.equals(this$aLocale, other$aLocale)) { + return false; + } + if (!java.util.Arrays.equals(this.getLongArray(), other.getLongArray())) { + return false; + } + if (this.getAShort() != other.getAShort()) { + return false; + } + if (!java.util.Arrays.equals(this.getShortArray(), other.getShortArray())) { + return false; + } + final Object this$aPeriod = this.getAPeriod(); + final Object other$aPeriod = other.getAPeriod(); + if (!Objects.equals(this$aPeriod, other$aPeriod)) { + return false; + } + final Object this$aDuration = this.getADuration(); + final Object other$aDuration = other.getADuration(); + if (!Objects.equals(this$aDuration, other$aDuration)) { + return false; + } + if (!java.util.Arrays.deepEquals(this.getStringArray(), other.getStringArray())) { + return false; + } + final Object this$listOfStrings = this.getListOfStrings(); + final Object other$listOfStrings = other.getListOfStrings(); + if (!Objects.equals(this$listOfStrings, other$listOfStrings)) { + return false; + } + final Object this$setOfStrings = this.getSetOfStrings(); + final Object other$setOfStrings = other.getSetOfStrings(); + if (!Objects.equals(this$setOfStrings, other$setOfStrings)) { + return false; + } + final Object this$anInstant = this.getAnInstant(); + final Object other$anInstant = other.getAnInstant(); + if (!Objects.equals(this$anInstant, other$anInstant)) { + return false; + } + final Object this$aUUID = this.getAUUID(); + final Object other$aUUID = other.getAUUID(); + if (!Objects.equals(this$aUUID, other$aUUID)) { + return false; + } + final Object this$aURL = this.getAURL(); + final Object other$aURL = other.getAURL(); + if (!Objects.equals(this$aURL, other$aURL)) { + return false; + } + final Object this$aURI = this.getAURI(); + final Object other$aURI = other.getAURI(); + if (!Objects.equals(this$aURI, other$aURI)) { + return false; + } + final Object this$anEnum = this.getAnEnum(); + final Object other$anEnum = other.getAnEnum(); + if (!Objects.equals(this$anEnum, other$anEnum)) { + return false; + } + if (!java.util.Arrays.deepEquals(this.getAnArrayOfEnums(), other.getAnArrayOfEnums())) { + return false; + } + final Object this$listOfDoubles = this.getListOfDoubles(); + final Object other$listOfDoubles = other.getListOfDoubles(); + if (!Objects.equals(this$listOfDoubles, other$listOfDoubles)) { + return false; + } + final Object this$aCollectionOfEnums = this.getACollectionOfEnums(); + final Object other$aCollectionOfEnums = other.getACollectionOfEnums(); + if (!Objects.equals(this$aCollectionOfEnums, other$aCollectionOfEnums)) { + return false; + } + final Object this$aTimeZone = this.getATimeZone(); + final Object other$aTimeZone = other.getATimeZone(); + if (!Objects.equals(this$aTimeZone, other$aTimeZone)) { + return false; + } + final Object this$aZoneId = this.getAZoneId(); + final Object other$aZoneId = other.getAZoneId(); + if (!Objects.equals(this$aZoneId, other$aZoneId)) { + return false; + } + final Object this$aZeroPeriod = this.getAZeroPeriod(); + final Object other$aZeroPeriod = other.getAZeroPeriod(); + if (!Objects.equals(this$aZeroPeriod, other$aZeroPeriod)) { + return false; + } + final Object this$aZeroDuration = this.getAZeroDuration(); + final Object other$aZeroDuration = other.getAZeroDuration(); + if (!Objects.equals(this$aZeroDuration, other$aZeroDuration)) { + return false; + } + + final Object this$aVector = this.getAVector(); + final Object other$aVector = other.getAVector(); + return Objects.equals(this$aVector, other$aVector); + } + + @Override + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $id = this.getId(); + result = (result * PRIME) + (($id != null) ? $id.hashCode() : 43); + result = result * PRIME + java.util.Arrays.hashCode(this.getBooleanArray()); + result = result * PRIME + this.getAByte(); + result = result * PRIME + this.getAChar(); + result = result * PRIME + java.util.Arrays.hashCode(this.getCharArray()); + final Object $aDate = this.getADate(); + result = (result * PRIME) + (($aDate != null) ? $aDate.hashCode() : 43); + final Object $aBigDecimal = this.getABigDecimal(); + result = (result * PRIME) + (($aBigDecimal != null) ? $aBigDecimal.hashCode() : 43); + final Object $aBigInteger = this.getABigInteger(); + result = (result * PRIME) + (($aBigInteger != null) ? $aBigInteger.hashCode() : 43); + result = result * PRIME + java.util.Arrays.hashCode(this.getDoubleArray()); + result = result * PRIME + Float.floatToIntBits(this.getAFloat()); + result = result * PRIME + java.util.Arrays.hashCode(this.getFloatArray()); + result = result * PRIME + this.getAnInt(); + result = result * PRIME + java.util.Arrays.hashCode(this.getIntArray()); + final Object $aLocale = this.getALocale(); + result = (result * PRIME) + (($aLocale != null) ? $aLocale.hashCode() : 43); + result = result * PRIME + java.util.Arrays.hashCode(this.getLongArray()); + result = result * PRIME + this.getAShort(); + result = result * PRIME + java.util.Arrays.hashCode(this.getShortArray()); + final Object $aPeriod = this.getAPeriod(); + result = (result * PRIME) + (($aPeriod != null) ? $aPeriod.hashCode() : 43); + final Object $aDuration = this.getADuration(); + result = (result * PRIME) + (($aDuration != null) ? $aDuration.hashCode() : 43); + result = result * PRIME + java.util.Arrays.deepHashCode(this.getStringArray()); + final Object $listOfStrings = this.getListOfStrings(); + result = (result * PRIME) + (($listOfStrings != null) ? $listOfStrings.hashCode() : 43); + final Object $setOfStrings = this.getSetOfStrings(); + result = (result * PRIME) + (($setOfStrings != null) ? $setOfStrings.hashCode() : 43); + final Object $anInstant = this.getAnInstant(); + result = (result * PRIME) + (($anInstant != null) ? $anInstant.hashCode() : 43); + final Object $aUUID = this.getAUUID(); + result = (result * PRIME) + (($aUUID != null) ? $aUUID.hashCode() : 43); + final Object $aURL = this.getAURL(); + result = (result * PRIME) + (($aURL != null) ? $aURL.hashCode() : 43); + final Object $aURI = this.getAURI(); + result = (result * PRIME) + (($aURI != null) ? $aURI.hashCode() : 43); + final Object $anEnum = this.getAnEnum(); + result = (result * PRIME) + (($anEnum != null) ? $anEnum.hashCode() : 43); + result = result * PRIME + java.util.Arrays.deepHashCode(this.getAnArrayOfEnums()); + final Object $listOfDoubles = this.getListOfDoubles(); + result = (result * PRIME) + (($listOfDoubles != null) ? $listOfDoubles.hashCode() : 43); + final Object $aCollectionOfEnums = this.getACollectionOfEnums(); + result = (result * PRIME) + (($aCollectionOfEnums != null) ? $aCollectionOfEnums.hashCode() : 43); + final Object $aTimeZone = this.getATimeZone(); + result = (result * PRIME) + (($aTimeZone != null) ? $aTimeZone.hashCode() : 43); + final Object $aZoneId = this.getAZoneId(); + result = (result * PRIME) + (($aZoneId != null) ? $aZoneId.hashCode() : 43); + final Object $aZeroPeriod = this.getAZeroPeriod(); + result = (result * PRIME) + (($aZeroPeriod != null) ? $aZeroPeriod.hashCode() : 43); + final Object $aZeroDuration = this.getAZeroDuration(); + result = (result * PRIME) + (($aZeroDuration != null) ? $aZeroDuration.hashCode() : 43); + final Object $aVector = this.getAVector(); + result = (result * PRIME) + (($aVector != null) ? $aVector.hashCode() : 43); + return result; + } + + @Override + public String toString() { + return "ThingWithAllAdditionalTypes(id=" + this.getId() + ", booleanArray=" + + java.util.Arrays.toString(this.getBooleanArray()) + ", aByte=" + this.getAByte() + ", aChar=" + + this.getAChar() + ", charArray=" + java.util.Arrays.toString(this.getCharArray()) + ", aDate=" + + this.getADate() + ", aBigDecimal=" + this.getABigDecimal() + ", aBigInteger=" + this.getABigInteger() + + ", doubleArray=" + java.util.Arrays.toString(this.getDoubleArray()) + ", aFloat=" + this.getAFloat() + + ", floatArray=" + java.util.Arrays.toString(this.getFloatArray()) + ", anInt=" + this.getAnInt() + + ", intArray=" + java.util.Arrays.toString(this.getIntArray()) + ", aLocale=" + this.getALocale() + + ", longArray=" + java.util.Arrays.toString(this.getLongArray()) + ", aShort=" + this.getAShort() + + ", shortArray=" + java.util.Arrays.toString(this.getShortArray()) + ", aPeriod=" + this.getAPeriod() + + ", aDuration=" + this.getADuration() + ", stringArray=" + + java.util.Arrays.deepToString(this.getStringArray()) + ", listOfStrings=" + this.getListOfStrings() + + ", setOfStrings=" + this.getSetOfStrings() + ", anInstant=" + this.getAnInstant() + ", aUUID=" + + this.getAUUID() + ", aURL=" + this.getAURL() + ", aURI=" + this.getAURI() + ", anEnum=" + + this.getAnEnum() + ", anArrayOfEnums=" + java.util.Arrays.deepToString(this.getAnArrayOfEnums()) + + ", listOfDoubles=" + this.getListOfDoubles() + ", aCollectionOfEnums=" + this.getACollectionOfEnums() + + ", aTimeZone=" + this.getATimeZone() + ", aZoneId=" + this.getAZoneId() + ", aZeroPeriod=" + + this.getAZeroPeriod() + ", aZeroDuration=" + this.getAZeroDuration() + ")"; + } + + enum SomeEnum { + + ValueA, ValueB, TheUsualMisfit, ValueC + + } + /** * the builder */ public static class ThingWithAllAdditionalTypesBuilder { + private Long id; + private boolean[] booleanArray; + private byte aByte; + private char aChar; + private char[] charArray; + private Date aDate; + private BigDecimal aBigDecimal; + private BigInteger aBigInteger; + private double[] doubleArray; + private float aFloat; + private float[] floatArray; + private int anInt; + private int[] intArray; + private Locale aLocale; + private long[] longArray; + private short aShort; + private short[] shortArray; + private Period aPeriod; + private Duration aDuration; + private String[] stringArray; + private List listOfStrings; + private Set setOfStrings; + private Instant anInstant; + private UUID aUUID; + private URL aURL; + private URI aURI; + private SomeEnum anEnum; + private SomeEnum[] anArrayOfEnums; + private List listOfDoubles; + private List aCollectionOfEnums; + private TimeZone aTimeZone; + private ZoneId aZoneId; + private Period aZeroPeriod; + private Duration aZeroDuration; + private Vector aVector; ThingWithAllAdditionalTypesBuilder() { @@ -894,11 +960,35 @@ public class ThingWithAllAdditionalTypes { } public ThingWithAllAdditionalTypes build() { - return new ThingWithAllAdditionalTypes(this.id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration, this.aVector); + return new ThingWithAllAdditionalTypes(this.id, this.booleanArray, this.aByte, this.aChar, this.charArray, + this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, + this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, + this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, + this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, + this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration, this.aVector); } + @Override public String toString() { - return "ThingWithAllAdditionalTypes.ThingWithAllAdditionalTypesBuilder(id=" + this.id + ", booleanArray=" + java.util.Arrays.toString(this.booleanArray) + ", aByte=" + this.aByte + ", aChar=" + this.aChar + ", charArray=" + java.util.Arrays.toString(this.charArray) + ", aDate=" + this.aDate + ", aBigDecimal=" + this.aBigDecimal + ", aBigInteger=" + this.aBigInteger + ", doubleArray=" + java.util.Arrays.toString(this.doubleArray) + ", aFloat=" + this.aFloat + ", floatArray=" + java.util.Arrays.toString(this.floatArray) + ", anInt=" + this.anInt + ", intArray=" + java.util.Arrays.toString(this.intArray) + ", aLocale=" + this.aLocale + ", longArray=" + java.util.Arrays.toString(this.longArray) + ", aShort=" + this.aShort + ", shortArray=" + java.util.Arrays.toString(this.shortArray) + ", aPeriod=" + this.aPeriod + ", aDuration=" + this.aDuration + ", stringArray=" + java.util.Arrays.deepToString(this.stringArray) + ", listOfStrings=" + this.listOfStrings + ", setOfStrings=" + this.setOfStrings + ", anInstant=" + this.anInstant + ", aUUID=" + this.aUUID + ", aURL=" + this.aURL + ", aURI=" + this.aURI + ", anEnum=" + this.anEnum + ", anArrayOfEnums=" + java.util.Arrays.deepToString(this.anArrayOfEnums) + ", listOfDoubles=" + this.listOfDoubles + ", aCollectionOfEnums=" + this.aCollectionOfEnums + ", aTimeZone=" + this.aTimeZone + ", aZoneId=" + this.aZoneId + ", aZeroPeriod=" + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + ", aVector=" + this.aVector + ")"; + return "ThingWithAllAdditionalTypes.ThingWithAllAdditionalTypesBuilder(id=" + this.id + ", booleanArray=" + + java.util.Arrays.toString(this.booleanArray) + ", aByte=" + this.aByte + ", aChar=" + this.aChar + + ", charArray=" + java.util.Arrays.toString(this.charArray) + ", aDate=" + this.aDate + + ", aBigDecimal=" + this.aBigDecimal + ", aBigInteger=" + this.aBigInteger + ", doubleArray=" + + java.util.Arrays.toString(this.doubleArray) + ", aFloat=" + this.aFloat + ", floatArray=" + + java.util.Arrays.toString(this.floatArray) + ", anInt=" + this.anInt + ", intArray=" + + java.util.Arrays.toString(this.intArray) + ", aLocale=" + this.aLocale + ", longArray=" + + java.util.Arrays.toString(this.longArray) + ", aShort=" + this.aShort + ", shortArray=" + + java.util.Arrays.toString(this.shortArray) + ", aPeriod=" + this.aPeriod + ", aDuration=" + + this.aDuration + ", stringArray=" + java.util.Arrays.deepToString(this.stringArray) + + ", listOfStrings=" + this.listOfStrings + ", setOfStrings=" + this.setOfStrings + ", anInstant=" + + this.anInstant + ", aUUID=" + this.aUUID + ", aURL=" + this.aURL + ", aURI=" + this.aURI + + ", anEnum=" + this.anEnum + ", anArrayOfEnums=" + + java.util.Arrays.deepToString(this.anArrayOfEnums) + ", listOfDoubles=" + this.listOfDoubles + + ", aCollectionOfEnums=" + this.aCollectionOfEnums + ", aTimeZone=" + this.aTimeZone + ", aZoneId=" + + this.aZoneId + ", aZeroPeriod=" + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + + ", aVector=" + this.aVector + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java index 6ff313e9d..633899b78 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java @@ -24,6 +24,7 @@ import java.util.function.BiFunction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; + import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter; import org.springframework.data.neo4j.core.schema.CompositeProperty; @@ -40,32 +41,8 @@ import org.springframework.data.neo4j.core.schema.Relationship; @Node("CompositeProperties") public class ThingWithCompositeProperties { - /** - * Map by enum key. - */ - public enum EnumA { - VALUE_AA - } - - /** - * Map by enum key. - */ - public enum EnumB { - VALUE_BA, - VALUE_BB { - @Override - public String toString() { - return "Ich bin superwitzig."; - } - }; - - @Override - public String toString() { - return super.name() + " deliberately screw the enum combo toString/name."; - } - } - - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @CompositeProperty @@ -92,13 +69,122 @@ public class ThingWithCompositeProperties { @Relationship("IRRELEVANT_TYPE") private RelationshipWithCompositeProperties relationship; + @CompositeProperty(converter = SomeOtherDTOToMapConverter.class, prefix = "dto") + private SomeOtherDTO someOtherDTO; + + public Long getId() { + return this.id; + } + + public Map getSomeDates() { + return this.someDates; + } + + public void setSomeDates(Map someDates) { + this.someDates = someDates; + } + + public Map getSomeOtherDates() { + return this.someOtherDates; + } + + public void setSomeOtherDates(Map someOtherDates) { + this.someOtherDates = someOtherDates; + } + + public Map getCustomTypeMap() { + return this.customTypeMap; + } + + public void setCustomTypeMap(Map customTypeMap) { + this.customTypeMap = customTypeMap; + } + + public Map getSomeDatesByEnumA() { + return this.someDatesByEnumA; + } + + public void setSomeDatesByEnumA(Map someDatesByEnumA) { + this.someDatesByEnumA = someDatesByEnumA; + } + + public Map getSomeDatesByEnumB() { + return this.someDatesByEnumB; + } + + public void setSomeDatesByEnumB(Map someDatesByEnumB) { + this.someDatesByEnumB = someDatesByEnumB; + } + + public Map getDatesWithTransformedKey() { + return this.datesWithTransformedKey; + } + + public void setDatesWithTransformedKey(Map datesWithTransformedKey) { + this.datesWithTransformedKey = datesWithTransformedKey; + } + + public Map getDatesWithTransformedKeyAndEnum() { + return this.datesWithTransformedKeyAndEnum; + } + + public void setDatesWithTransformedKeyAndEnum(Map datesWithTransformedKeyAndEnum) { + this.datesWithTransformedKeyAndEnum = datesWithTransformedKeyAndEnum; + } + + public SomeOtherDTO getSomeOtherDTO() { + return this.someOtherDTO; + } + + public void setSomeOtherDTO(SomeOtherDTO someOtherDTO) { + this.someOtherDTO = someOtherDTO; + } + + public RelationshipWithCompositeProperties getRelationship() { + return this.relationship; + } + + public void setRelationship(RelationshipWithCompositeProperties relationship) { + this.relationship = relationship; + } + + /** + * Map by enum key. + */ + public enum EnumA { + + VALUE_AA + + } + + /** + * Map by enum key. + */ + public enum EnumB { + + VALUE_BA, VALUE_BB { + @Override + public String toString() { + return "Ich bin superwitzig."; + } + }; + + @Override + public String toString() { + return super.name() + " deliberately screw the enum combo toString/name."; + } + + } + /** * Arbitrary DTO. */ public static class SomeOtherDTO { final String x; + final Long y; + final Double z; public SomeOtherDTO(String x, Long y, Double z) { @@ -116,99 +202,14 @@ public class ThingWithCompositeProperties { return false; } SomeOtherDTO that = (SomeOtherDTO) o; - return x.equals(that.x) && - y.equals(that.y) && - z.equals(that.z); + return this.x.equals(that.x) && this.y.equals(that.y) && this.z.equals(that.z); } @Override public int hashCode() { - return Objects.hash(x, y, z); + return Objects.hash(this.x, this.y, this.z); } - } - @CompositeProperty(converter = SomeOtherDTOToMapConverter.class, prefix = "dto") - private SomeOtherDTO someOtherDTO; - - public Long getId() { - return id; - } - - public Map getSomeDates() { - return someDates; - } - - public void setSomeDates(Map someDates) { - this.someDates = someDates; - } - - public Map getSomeOtherDates() { - return someOtherDates; - } - - public void setSomeOtherDates(Map someOtherDates) { - this.someOtherDates = someOtherDates; - } - - public Map getCustomTypeMap() { - return customTypeMap; - } - - public void setCustomTypeMap( - Map customTypeMap) { - this.customTypeMap = customTypeMap; - } - - public Map getSomeDatesByEnumA() { - return someDatesByEnumA; - } - - public void setSomeDatesByEnumA( - Map someDatesByEnumA) { - this.someDatesByEnumA = someDatesByEnumA; - } - - public Map getSomeDatesByEnumB() { - return someDatesByEnumB; - } - - public void setSomeDatesByEnumB( - Map someDatesByEnumB) { - this.someDatesByEnumB = someDatesByEnumB; - } - - public Map getDatesWithTransformedKey() { - return datesWithTransformedKey; - } - - public void setDatesWithTransformedKey(Map datesWithTransformedKey) { - this.datesWithTransformedKey = datesWithTransformedKey; - } - - public Map getDatesWithTransformedKeyAndEnum() { - return datesWithTransformedKeyAndEnum; - } - - public void setDatesWithTransformedKeyAndEnum( - Map datesWithTransformedKeyAndEnum) { - this.datesWithTransformedKeyAndEnum = datesWithTransformedKeyAndEnum; - } - - public SomeOtherDTO getSomeOtherDTO() { - return someOtherDTO; - } - - public void setSomeOtherDTO(SomeOtherDTO someOtherDTO) { - this.someOtherDTO = someOtherDTO; - } - - public RelationshipWithCompositeProperties getRelationship() { - return relationship; - } - - public void setRelationship( - RelationshipWithCompositeProperties relationship) { - this.relationship = relationship; } static class LowerCasePropertiesFilter implements BiFunction { @@ -225,6 +226,7 @@ public class ThingWithCompositeProperties { default -> throw new IllegalArgumentException(); }; } + } /** @@ -240,7 +242,8 @@ public class ThingWithCompositeProperties { decomposed.put("x", Values.NULL); decomposed.put("y", Values.NULL); decomposed.put("z", Values.NULL); - } else { + } + else { decomposed.put("x", Values.value(property.x)); decomposed.put("y", Values.value(property.y)); decomposed.put("z", Values.value(property.z)); @@ -250,9 +253,10 @@ public class ThingWithCompositeProperties { @Override public SomeOtherDTO compose(Map source, Neo4jConversionService conversionService) { - return source.isEmpty() ? - null : - new SomeOtherDTO(source.get("x").asString(), source.get("y").asLong(), source.get("z").asDouble()); + return source.isEmpty() ? null : new SomeOtherDTO(source.get("x").asString(), source.get("y").asLong(), + source.get("z").asDouble()); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCustomTypes.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCustomTypes.java index b7babd6ec..4b460ae40 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCustomTypes.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCustomTypes.java @@ -23,6 +23,7 @@ import java.util.Set; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.internal.value.StringValue; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.neo4j.core.schema.GeneratedValue; @@ -38,7 +39,9 @@ import org.springframework.data.neo4j.core.support.DateString; @Node("CustomTypes") public class ThingWithCustomTypes { - @Id @GeneratedValue private final Long id; + @Id + @GeneratedValue + private final Long id; private CustomType customType; @@ -60,42 +63,42 @@ public class ThingWithCustomTypes { } public CustomType getCustomType() { - return customType; + return this.customType; + } + + public Long getId() { + return this.id; + } + + public Date getDateAsLong() { + return this.dateAsLong; } public void setDateAsLong(Date dateAsLong) { this.dateAsLong = dateAsLong; } - public Long getId() { - return id; - } - - public Date getDateAsLong() { - return dateAsLong; - } - public Date getDateAsString() { - return dateAsString; + return this.dateAsString; } /** * Custom type to convert */ - public static class CustomType { + public static final class CustomType { private final String value; + private CustomType(String value) { + this.value = value; + } + public static CustomType of(String value) { return new CustomType(value); } public String getValue() { - return value; - } - - private CustomType(String value) { - this.value = value; + return this.value; } @Override @@ -107,13 +110,14 @@ public class ThingWithCustomTypes { return false; } CustomType that = (CustomType) o; - return value.equals(that.value); + return this.value.equals(that.value); } @Override public int hashCode() { - return Objects.hash(value); + return Objects.hash(this.value); } + } /** @@ -134,30 +138,33 @@ public class ThingWithCustomTypes { if (StringValue.class.isAssignableFrom(sourceType.getType())) { return CustomType.of(((StringValue) source).asString()); - } else { + } + else { return Values.value(((CustomType) source).getValue()); } } + } /** * A type that is not bound anywhere but has a converter */ - public static class DifferentType { + public static final class DifferentType { private final String value; - public static DifferentType of(String value) { - return new DifferentType(value); - } - private DifferentType(String value) { this.value = value; } - public String getValue() { - return value; + public static DifferentType of(String value) { + return new DifferentType(value); } + + public String getValue() { + return this.value; + } + } /** @@ -178,9 +185,12 @@ public class ThingWithCustomTypes { if (Value.class.isAssignableFrom(sourceType.getType())) { return CustomType.of(((Value) source).asString()); - } else { + } + else { return Values.value(((DifferentType) source).getValue()); } } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/OptimisticLockingOfSelfReferencesIT.java b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/OptimisticLockingOfSelfReferencesIT.java index 41d4d4ffc..66c9522e7 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/OptimisticLockingOfSelfReferencesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/OptimisticLockingOfSelfReferencesIT.java @@ -15,8 +15,6 @@ */ package org.springframework.data.neo4j.integration.versioned_self_references; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collections; import java.util.function.Supplier; @@ -25,18 +23,21 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; 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.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -53,7 +54,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.save(r1); + this.neo4jTemplate.save(r1); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -68,7 +69,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.saveAll(Collections.singletonList(r1)); + this.neo4jTemplate.saveAll(Collections.singletonList(r1)); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -83,7 +84,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.saveAll(Arrays.asList(r1, r2)); + this.neo4jTemplate.saveAll(Arrays.asList(r1, r2)); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -97,12 +98,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { Long id1 = createInstance(type); Long id2 = createInstance(type); - T r1 = neo4jTemplate.findById(id1, type).get(); - T r2 = neo4jTemplate.findById(id2, type).get(); + T r1 = this.neo4jTemplate.findById(id1, type).get(); + T r2 = this.neo4jTemplate.findById(id2, type).get(); r1.relate(r2); - neo4jTemplate.save(r1); + this.neo4jTemplate.save(r1); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -116,12 +117,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { Long id1 = createInstance(type); Long id2 = createInstance(type); - T r1 = neo4jTemplate.findById(id1, type).get(); - T r2 = neo4jTemplate.findById(id2, type).get(); + T r1 = this.neo4jTemplate.findById(id1, type).get(); + T r2 = this.neo4jTemplate.findById(id2, type).get(); r1.relate(r2); - neo4jTemplate.saveAll(Collections.singletonList(r1)); + this.neo4jTemplate.saveAll(Collections.singletonList(r1)); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -135,12 +136,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { Long id1 = createInstance(type); Long id2 = createInstance(type); - T r1 = neo4jTemplate.findById(id1, type).get(); - T r2 = neo4jTemplate.findById(id2, type).get(); + T r1 = this.neo4jTemplate.findById(id1, type).get(); + T r2 = this.neo4jTemplate.findById(id2, type).get(); r1.relate(r2); - neo4jTemplate.saveAll(Arrays.asList(r1, r2)); + this.neo4jTemplate.saveAll(Arrays.asList(r1, r2)); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -153,12 +154,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { long[] ids = createRelatedInstances(type); - T r1 = neo4jTemplate.findById(ids[0], type).get(); - T r2 = neo4jTemplate.findById(ids[1], type).get(); + T r1 = this.neo4jTemplate.findById(ids[0], type).get(); + T r2 = this.neo4jTemplate.findById(ids[1], type).get(); r1.relate(r2); - neo4jTemplate.save(r1); + this.neo4jTemplate.save(r1); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -171,12 +172,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { long[] ids = createRelatedInstances(type); - T r1 = neo4jTemplate.findById(ids[0], type).get(); - T r2 = neo4jTemplate.findById(ids[1], type).get(); + T r1 = this.neo4jTemplate.findById(ids[0], type).get(); + T r2 = this.neo4jTemplate.findById(ids[1], type).get(); r1.relate(r2); - neo4jTemplate.saveAll(Collections.singletonList(r1)); + this.neo4jTemplate.saveAll(Collections.singletonList(r1)); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -189,12 +190,12 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { long[] ids = createRelatedInstances(type); - T r1 = neo4jTemplate.findById(ids[0], type).get(); - T r2 = neo4jTemplate.findById(ids[1], type).get(); + T r1 = this.neo4jTemplate.findById(ids[0], type).get(); + T r2 = this.neo4jTemplate.findById(ids[1], type).get(); r1.relate(r2); - neo4jTemplate.saveAll(Arrays.asList(r1, r2)); + this.neo4jTemplate.saveAll(Arrays.asList(r1, r2)); assertDatabase(1L, type, r1); assertDatabase(1L, type, r2); @@ -206,38 +207,38 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { int ringSize = 5; VersionedExternalIdWithEquals start = createRing(ringSize); - start = neo4jTemplate.save(start); + start = this.neo4jTemplate.save(start); - assertThat(neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class)).hasValueSatisfying( - root -> { - int traversedObjects = traverseRing(root, next -> { - assertThat(next.getRelatedObjects()).hasSize(2); - assertThat(next.getVersion()).isEqualTo(0L); - }); - assertThat(traversedObjects).isEqualTo(ringSize); + assertThat(this.neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class)) + .hasValueSatisfying(root -> { + int traversedObjects = traverseRing(root, next -> { + assertThat(next.getRelatedObjects()).hasSize(2); + assertThat(next.getVersion()).isEqualTo(0L); }); + assertThat(traversedObjects).isEqualTo(ringSize); + }); String newName = "A new beginning"; start.setName(newName); - neo4jTemplate.saveAs(start, NameOnly.class); + this.neo4jTemplate.saveAs(start, NameOnly.class); - assertThat(neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class)).hasValueSatisfying( - root -> { - assertThat(root.getName()).isEqualTo(newName); - assertThat(root.getVersion()).isEqualTo(1L); + assertThat(this.neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class)) + .hasValueSatisfying(root -> { + assertThat(root.getName()).isEqualTo(newName); + assertThat(root.getVersion()).isEqualTo(1L); - int traversedObjects = traverseRing(root, next -> { - assertThat(next.getRelatedObjects()).hasSize(2); - assertThat(next.getVersion()).isEqualTo(next.getName().equals(newName) ? 1L : 0L); - }); - assertThat(traversedObjects).isEqualTo(ringSize); + int traversedObjects = traverseRing(root, next -> { + assertThat(next.getRelatedObjects()).hasSize(2); + assertThat(next.getVersion()).isEqualTo(next.getName().equals(newName) ? 1L : 0L); }); + assertThat(traversedObjects).isEqualTo(ringSize); + }); } private > void assertLoadingViaSDN(Class type, Long... ids) { for (Long id : ids) { - assertThat(neo4jTemplate.findById(id, type)).isPresent(); + assertThat(this.neo4jTemplate.findById(id, type)).isPresent(); } } @@ -246,7 +247,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { static class Config extends Neo4jImperativeTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -260,6 +261,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -269,5 +271,7 @@ class OptimisticLockingOfSelfReferencesIT extends TestBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/ReactiveOptimisticLockingOfSelfReferencesIT.java b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/ReactiveOptimisticLockingOfSelfReferencesIT.java index 40aa3177a..91b049575 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/ReactiveOptimisticLockingOfSelfReferencesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/ReactiveOptimisticLockingOfSelfReferencesIT.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.integration.versioned_self_references; -import static org.assertj.core.api.Assertions.assertThat; - -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; -import reactor.test.StepVerifier; - import java.util.Arrays; import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; @@ -30,6 +25,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.neo4j.driver.Driver; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,9 +35,12 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -57,8 +57,7 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.save(r1) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.save(r1).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -73,8 +72,10 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.saveAll(Collections.singletonList(r1)) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.saveAll(Collections.singletonList(r1)) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -89,8 +90,7 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { T r2 = f.get(); r1.relate(r2); - neo4jTemplate.saveAll(Arrays.asList(r1, r2)) - .as(StepVerifier::create).expectNextCount(2L).verifyComplete(); + this.neo4jTemplate.saveAll(Arrays.asList(r1, r2)).as(StepVerifier::create).expectNextCount(2L).verifyComplete(); assertDatabase(0L, type, r1); assertDatabase(0L, type, r2); @@ -106,17 +106,20 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(id1, type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(id2, type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(id1, type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(id2, type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.save(r1.get()) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.save(r1.get()).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -132,17 +135,24 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(id1, type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(id2, type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(id1, type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(id2, type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.saveAll(Collections.singletonList(r1).stream().map(AtomicReference::get).collect(Collectors.toList())) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate + .saveAll(Collections.singletonList(r1).stream().map(AtomicReference::get).collect(Collectors.toList())) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -158,17 +168,24 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(id1, type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(id2, type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(id1, type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(id2, type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.saveAll(Arrays.asList(r1, r2).stream().map(AtomicReference::get).collect(Collectors.toList())) - .as(StepVerifier::create).expectNextCount(2L).verifyComplete(); + this.neo4jTemplate + .saveAll(Arrays.asList(r1, r2).stream().map(AtomicReference::get).collect(Collectors.toList())) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -183,17 +200,20 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(ids[0], type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(ids[1], type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(ids[0], type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(ids[1], type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.save(r1.get()) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.save(r1.get()).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -208,17 +228,24 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(ids[0], type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(ids[1], type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(ids[0], type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(ids[1], type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.saveAll(Arrays.asList(r1, r2).stream().map(AtomicReference::get).collect(Collectors.toList())) - .as(StepVerifier::create).expectNextCount(2L).verifyComplete(); + this.neo4jTemplate + .saveAll(Arrays.asList(r1, r2).stream().map(AtomicReference::get).collect(Collectors.toList())) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -233,17 +260,24 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference r1 = new AtomicReference<>(); AtomicReference r2 = new AtomicReference<>(); - neo4jTemplate.findById(ids[0], type) - .doOnNext(r1::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(ids[1], type) - .doOnNext(r2::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(ids[0], type) + .doOnNext(r1::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + this.neo4jTemplate.findById(ids[1], type) + .doOnNext(r2::set) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); r1.get().relate(r2.get()); - neo4jTemplate.saveAll(Collections.singletonList(r1).stream().map(AtomicReference::get).collect(Collectors.toList())) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate + .saveAll(Collections.singletonList(r1).stream().map(AtomicReference::get).collect(Collectors.toList())) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); assertDatabase(1L, type, r1.get()); assertDatabase(1L, type, r2.get()); @@ -257,48 +291,46 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { AtomicReference ref = new AtomicReference<>(); VersionedExternalIdWithEquals start = createRing(ringSize); - neo4jTemplate.save(start) - .doOnNext(ref::set) - .as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.save(start).doOnNext(ref::set).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); start = ref.get(); - neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class) - .as(StepVerifier::create) - .expectNextMatches(root -> { - int traversedObjects = traverseRing(root, next -> { - assertThat(next.getRelatedObjects()).hasSize(2); - assertThat(next.getVersion()).isEqualTo(0L); - }); - assertThat(traversedObjects).isEqualTo(ringSize); - return true; - }) - .verifyComplete(); + this.neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class) + .as(StepVerifier::create) + .expectNextMatches(root -> { + int traversedObjects = traverseRing(root, next -> { + assertThat(next.getRelatedObjects()).hasSize(2); + assertThat(next.getVersion()).isEqualTo(0L); + }); + assertThat(traversedObjects).isEqualTo(ringSize); + return true; + }) + .verifyComplete(); String newName = "A new beginning"; start.setName(newName); - neo4jTemplate.saveAs(start, NameOnly.class).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.saveAs(start, NameOnly.class).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class) - .as(StepVerifier::create) - .expectNextMatches(root -> { - assertThat(root.getName()).isEqualTo(newName); - assertThat(root.getVersion()).isEqualTo(1L); + this.neo4jTemplate.findById(start.getId(), VersionedExternalIdWithEquals.class) + .as(StepVerifier::create) + .expectNextMatches(root -> { + assertThat(root.getName()).isEqualTo(newName); + assertThat(root.getVersion()).isEqualTo(1L); - int traversedObjects = traverseRing(root, next -> { - assertThat(next.getRelatedObjects()).hasSize(2); - assertThat(next.getVersion()).isEqualTo(next.getName().equals(newName) ? 1L : 0L); - }); - assertThat(traversedObjects).isEqualTo(ringSize); - return true; - }) - .verifyComplete(); + int traversedObjects = traverseRing(root, next -> { + assertThat(next.getRelatedObjects()).hasSize(2); + assertThat(next.getVersion()).isEqualTo(next.getName().equals(newName) ? 1L : 0L); + }); + assertThat(traversedObjects).isEqualTo(ringSize); + return true; + }) + .verifyComplete(); } private > void assertLoadingViaSDN(Class type, Long... ids) { for (Long id : ids) { - neo4jTemplate.findById(id, type).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + this.neo4jTemplate.findById(id, type).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); } } @@ -307,7 +339,7 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { static class Config extends Neo4jReactiveTestConfiguration { @Bean - public BookmarkCapture bookmarkCapture() { + BookmarkCapture bookmarkCapture() { return new BookmarkCapture(); } @@ -320,6 +352,7 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { } @Bean + @Override public Driver driver() { return neo4jConnectionSupport.getDriver(); @@ -329,5 +362,7 @@ class ReactiveOptimisticLockingOfSelfReferencesIT extends TestBase { public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/Relatable.java b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/Relatable.java index 0c7d6a7f7..af8469c70 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/Relatable.java +++ b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/Relatable.java @@ -28,4 +28,5 @@ interface Relatable { Collection getRelatedObjects(); void relate(T object); + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/TestBase.java b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/TestBase.java index 1e465f582..c884e31f4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/TestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/TestBase.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.versioned_self_references; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,18 +28,21 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.params.provider.Arguments; import org.neo4j.cypherdsl.core.Cypher; - import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.ResultStatement; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author Michael J. Simons */ @@ -50,24 +50,24 @@ import org.springframework.data.neo4j.test.Neo4jIntegrationTest; @TestMethodOrder(MethodOrderer.DisplayName.class) abstract class TestBase { - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - private static final Supplier sequenceGenerator = new Supplier<>() { private final AtomicLong source = new AtomicLong(0L); @Override public Long get() { - return source.incrementAndGet(); + return this.source.incrementAndGet(); } }; - @Autowired - private Driver driver; + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; @Autowired BookmarkCapture bookmarkCapture; + @Autowired + private Driver driver; + @BeforeAll protected static void clearDatabase(@Autowired BookmarkCapture bookmarkCapture) { @@ -78,12 +78,11 @@ abstract class TestBase { } static Stream typeAndNewInstanceSupplier() { - return Stream.of( - Arguments.arguments(VersionedExternalIdWithEquals.class, - (Supplier) () -> { - long id = sequenceGenerator.get(); - return new VersionedExternalIdWithEquals(id, "Instance" + id); - }), + return Stream.of(Arguments.arguments(VersionedExternalIdWithEquals.class, + (Supplier) () -> { + long id = sequenceGenerator.get(); + return new VersionedExternalIdWithEquals(id, "Instance" + id); + }), Arguments.arguments(VersionedExternalIdWithoutEquals.class, (Supplier) () -> { @@ -106,20 +105,20 @@ abstract class TestBase { Arguments.arguments(VersionedInternalIdListBased.class, (Supplier) () -> new VersionedInternalIdListBased( - "An object " + System.currentTimeMillis())) - ); + "An object " + System.currentTimeMillis()))); } static Stream typesForExistingInstanceSupplier() { - return Stream.of(VersionedExternalIdWithEquals.class, VersionedExternalIdWithoutEquals.class, - VersionedExternalIdListBased.class, - VersionedInternalIdWithEquals.class, VersionedInternalIdWithoutEquals.class, - VersionedInternalIdListBased.class).map(Arguments::of); + return Stream + .of(VersionedExternalIdWithEquals.class, VersionedExternalIdWithoutEquals.class, + VersionedExternalIdListBased.class, VersionedInternalIdWithEquals.class, + VersionedInternalIdWithoutEquals.class, VersionedInternalIdListBased.class) + .map(Arguments::of); } Long createInstance(Class type) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); Transaction tx = session.beginTransaction()) { Map properties = new HashMap<>(); @@ -134,8 +133,9 @@ abstract class TestBase { ResultStatement statement; if (isExternal) { statement = Cypher.create(nodeTemplate).returning(nodeTemplate.property("id")).build(); - } else { - //noinspection deprecation + } + else { + // noinspection deprecation statement = Cypher.create(nodeTemplate).returning(nodeTemplate.internalId()).build(); } @@ -147,7 +147,7 @@ abstract class TestBase { } long[] createRelatedInstances(Class type) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig()); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig()); Transaction tx = session.beginTransaction()) { String simpleName = type.getSimpleName(); @@ -167,18 +167,21 @@ abstract class TestBase { Node n2 = nodeTemplate.named("n2").withProperties(propertySupplier.get()); ResultStatement statement; if (isExternal) { - statement = Cypher.create(n1).create(n2) - .merge(n1.relationshipTo(n2, "RELATED")) - .merge(n2.relationshipTo(n1, "RELATED")) - .returning(n1.property("id"), n2.property("id")) - .build(); - } else { - //noinspection deprecation - statement = Cypher.create(n1).create(n2) - .merge(n1.relationshipTo(n2, "RELATED")) - .merge(n2.relationshipTo(n1, "RELATED")) - .returning(n1.internalId(), n2.internalId()) - .build(); + statement = Cypher.create(n1) + .create(n2) + .merge(n1.relationshipTo(n2, "RELATED")) + .merge(n2.relationshipTo(n1, "RELATED")) + .returning(n1.property("id"), n2.property("id")) + .build(); + } + else { + // noinspection deprecation + statement = Cypher.create(n1) + .create(n2) + .merge(n1.relationshipTo(n2, "RELATED")) + .merge(n2.relationshipTo(n1, "RELATED")) + .returning(n1.internalId(), n2.internalId()) + .build(); } Record record = tx.run(statement.getCypher(), statement.getCatalog().getParameters()).single(); @@ -195,9 +198,11 @@ abstract class TestBase { String simpleName = type.getSimpleName(); if (simpleName.contains("External")) { assertExternal(expectedVersion, type, root.getId()); - } else if (simpleName.contains("Internal")) { + } + else if (simpleName.contains("Internal")) { assertInternal(expectedVersion, type, root.getId()); - } else { + } + else { fail("Unsupported type: " + type); } } @@ -206,7 +211,8 @@ abstract class TestBase { VersionedExternalIdWithEquals start = new VersionedExternalIdWithEquals(sequenceGenerator.get(), "start"); VersionedExternalIdWithEquals previous = start; for (int i = 0; i < ringSize - 1; ++i) { - VersionedExternalIdWithEquals next = new VersionedExternalIdWithEquals(sequenceGenerator.get(), Integer.toString(i)); + VersionedExternalIdWithEquals next = new VersionedExternalIdWithEquals(sequenceGenerator.get(), + Integer.toString(i)); previous.relate(next); previous = next; } @@ -222,33 +228,31 @@ abstract class TestBase { assertion.accept(next); VersionedExternalIdWithEquals[] relatedObjects = next.getRelatedObjects() - .toArray(new VersionedExternalIdWithEquals[0]); + .toArray(new VersionedExternalIdWithEquals[0]); String nextName = Integer.toString(cnt++); if (relatedObjects[0].getName().equals(nextName)) { next = relatedObjects[0]; - } else if (relatedObjects[1].getName().equals(nextName)) { + } + else if (relatedObjects[1].getName().equals(nextName)) { next = relatedObjects[1]; - } else { + } + else { next = null; } - } while (next != null); + } + while (next != null); return cnt; } - interface NameOnly { - String getName(); - } - private void assertExternal(Long expectedVersion, Class type, Long id) { Node nodeTemplate = Cypher.node(type.getSimpleName()); Node n1 = nodeTemplate.named("n1"); Node n2 = nodeTemplate.named("n2"); - ResultStatement statement = - Cypher.match(n1.relationshipTo(n2, "RELATED")) - .where(n1.property("id").isEqualTo(Cypher.anonParameter(id)) - .and(n2.relationshipTo(n1, "RELATED"))) - .returning(n1.property("version")).build(); + ResultStatement statement = Cypher.match(n1.relationshipTo(n2, "RELATED")) + .where(n1.property("id").isEqualTo(Cypher.anonParameter(id)).and(n2.relationshipTo(n1, "RELATED"))) + .returning(n1.property("version")) + .build(); assertImpl(expectedVersion, statement); } @@ -258,23 +262,30 @@ abstract class TestBase { Node nodeTemplate = Cypher.node(type.getSimpleName()); Node n1 = nodeTemplate.named("n1"); Node n2 = nodeTemplate.named("n2"); - @SuppressWarnings("deprecation") ResultStatement statement = - Cypher.match(n1.relationshipTo(n2, "RELATED")) - .where(n1.internalId().isEqualTo(Cypher.anonParameter(id)) - .and(n2.relationshipTo(n1, "RELATED"))) - .returning(n1.property("version")).build(); + @SuppressWarnings("deprecation") + ResultStatement statement = Cypher.match(n1.relationshipTo(n2, "RELATED")) + .where(n1.internalId().isEqualTo(Cypher.anonParameter(id)).and(n2.relationshipTo(n1, "RELATED"))) + .returning(n1.property("version")) + .build(); assertImpl(expectedVersion, statement); } private void assertImpl(Long expectedVersion, ResultStatement resultStatement) { - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - List result = session.run(resultStatement.getCypher(), resultStatement.getCatalog().getParameters()).list(); - assertThat(result).hasSize(1) - .first().satisfies(record -> { - long version = record.get(0).asLong(); - assertThat(version).isEqualTo(expectedVersion); - }); + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + List result = session.run(resultStatement.getCypher(), resultStatement.getCatalog().getParameters()) + .list(); + assertThat(result).hasSize(1).first().satisfies(record -> { + long version = record.get(0).asLong(); + assertThat(version).isEqualTo(expectedVersion); + }); } } + + interface NameOnly { + + String getName(); + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/VersionedExternalIdListBased.java b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/VersionedExternalIdListBased.java index bee259490..8912d5b5c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/VersionedExternalIdListBased.java +++ b/src/test/java/org/springframework/data/neo4j/integration/versioned_self_references/VersionedExternalIdListBased.java @@ -31,11 +31,11 @@ class VersionedExternalIdListBased implements Relatable relatedObjects = new ArrayList<>(); @@ -46,27 +46,27 @@ class VersionedExternalIdListBased implements Relatable getRelatedObjects() { - return Collections.unmodifiableList(relatedObjects); + return Collections.unmodifiableList(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedExternalIdListBased)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that {@link #relate(VersionedExternalIdListBased)} + * cannot be ignored. In case that doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -79,4 +79,5 @@ class VersionedExternalIdListBased implements Relatable getRelatedObjects() { - return Collections.unmodifiableSet(relatedObjects); + return Collections.unmodifiableSet(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedExternalIdWithEquals)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that {@link #relate(VersionedExternalIdWithEquals)} + * cannot be ignored. In case that doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -96,19 +96,17 @@ class VersionedExternalIdWithEquals implements Relatable relatedObjects = new HashSet<>(); @@ -46,27 +46,28 @@ class VersionedExternalIdWithoutEquals implements Relatable getRelatedObjects() { - return Collections.unmodifiableSet(relatedObjects); + return Collections.unmodifiableSet(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedExternalIdWithoutEquals)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that + * {@link #relate(VersionedExternalIdWithoutEquals)} cannot be ignored. In case that + * doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -79,4 +80,5 @@ class VersionedExternalIdWithoutEquals implements Relatable { - @Id @GeneratedValue + private final String name; + + @Id + @GeneratedValue private Long id; @Version private Long version; - private final String name; - @Relationship(direction = Relationship.Direction.OUTGOING, type = "RELATED") private List relatedObjects = new ArrayList<>(); @@ -46,27 +47,27 @@ class VersionedInternalIdListBased implements Relatable getRelatedObjects() { - return Collections.unmodifiableList(relatedObjects); + return Collections.unmodifiableList(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedInternalIdListBased)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that {@link #relate(VersionedInternalIdListBased)} + * cannot be ignored. In case that doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -79,4 +80,5 @@ class VersionedInternalIdListBased implements Relatable { - @Id @GeneratedValue + private final String name; + + @Id + @GeneratedValue private Long id; @Version private Long version; - private final String name; - @Relationship(direction = Relationship.Direction.OUTGOING, type = "RELATED") private Set relatedObjects = new HashSet<>(); @@ -47,27 +48,27 @@ class VersionedInternalIdWithEquals implements Relatable getRelatedObjects() { - return Collections.unmodifiableSet(relatedObjects); + return Collections.unmodifiableSet(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedInternalIdWithEquals)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that {@link #relate(VersionedInternalIdWithEquals)} + * cannot be ignored. In case that doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -90,11 +91,12 @@ class VersionedInternalIdWithEquals implements Relatable { - @Id @GeneratedValue + private final String name; + + @Id + @GeneratedValue private Long id; @Version private Long version; - private final String name; - @Relationship(direction = Relationship.Direction.OUTGOING, type = "RELATED") private Set relatedObjects = new HashSet<>(); @@ -46,27 +47,28 @@ class VersionedInternalIdWithoutEquals implements Relatable getRelatedObjects() { - return Collections.unmodifiableSet(relatedObjects); + return Collections.unmodifiableSet(this.relatedObjects); } /** - * Called by SDN to set the related objects. In case of cyclic mapping, this can't be done via constructor. - * I personally would want the {@link #getRelatedObjects()} not to return a modifiable list, so that - * {@link #relate(VersionedInternalIdWithoutEquals)} cannot be ignored. In case that doesn't matter, a getter is enough. - * + * Called by SDN to set the related objects. In case of cyclic mapping, this can't be + * done via constructor. I personally would want the {@link #getRelatedObjects()} not + * to return a modifiable list, so that + * {@link #relate(VersionedInternalIdWithoutEquals)} cannot be ignored. In case that + * doesn't matter, a getter is enough. * @param relatedObjects New collection of related objects */ @SuppressWarnings("unused") @@ -79,4 +81,5 @@ class VersionedInternalIdWithoutEquals implements Relatable {} + interface RepositoryToBeExcluded extends Neo4jRepository { + + } @Configuration - @EnableNeo4jRepositories(considerNestedRepositories = true, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = RepositoryToBeExcluded.class)) + @EnableNeo4jRepositories(considerNestedRepositories = true, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = RepositoryToBeExcluded.class)) static class Config extends AbstractNeo4jConfig { @Bean @@ -88,6 +87,19 @@ class EnableNeo4jRepositoriesTests { public Driver driver() { return Mockito.mock(Driver.class); } + } + } + + @EnableNeo4jRepositories(BASE_PACKAGES_VALUE) + private final class EnableRepositoryConfigWithValue { + + } + + @EnableNeo4jRepositories(basePackages = BASE_PACKAGES_VALUE) + private final class EnableRepositoryConfigWithBasePackages { + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositoriesTests.java b/src/test/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositoriesTests.java index 9a3468ca7..2d31ce7c3 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositoriesTests.java +++ b/src/test/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositoriesTests.java @@ -15,12 +15,11 @@ */ package org.springframework.data.neo4j.repository.config; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.neo4j.driver.Driver; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -32,6 +31,8 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @@ -47,10 +48,13 @@ class EnableReactiveNeo4jRepositoriesTests { } interface RepositoryToBeExcluded extends ReactiveNeo4jRepository { + } @Configuration - @EnableReactiveNeo4jRepositories(considerNestedRepositories = true, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = RepositoryToBeExcluded.class)) + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = RepositoryToBeExcluded.class)) static class Config extends AbstractNeo4jConfig { @Bean @@ -58,6 +62,9 @@ class EnableReactiveNeo4jRepositoriesTests { public Driver driver() { return Mockito.mock(Driver.class); } + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java b/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTests.java similarity index 94% rename from src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java rename to src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTests.java index 88feb7db2..bb20bc3d1 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTests.java @@ -15,15 +15,14 @@ */ package org.springframework.data.neo4j.repository.config; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons - * @soundtrack Helge & Hardcore - Jazz */ -class StartupLoggerTest { +class StartupLoggerTests { @Test void startingMessageShouldFit() { @@ -32,4 +31,5 @@ class StartupLoggerTest { assertThat(message).matches( "Bootstrapping imperative Neo4j repositories based on an unknown version of SDN with Spring Data Commons .+ and Neo4j Driver .+\\."); } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTests.java similarity index 88% rename from src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTests.java index f626b9565..3968e618c 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/BoundingBoxTests.java @@ -15,21 +15,38 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.data.geo.Box; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class BoundingBoxTest { +class BoundingBoxTests { + + private static Stream polygonsToTest() { + return Stream.of( + Arguments.of(new Polygon(new Point(1, 1), new Point(5, 1), new Point(5, 5), new Point(5, 1)), + new Point(1, 1), new Point(5, 5)), + Arguments.of(new Polygon(new Point(3, 6), new Point(6, 2), new Point(8, 3), new Point(8, 6), + new Point(2, 9)), new Point(2, 2), new Point(8, 9)), + Arguments.of(new Polygon(new Point(3, 4), new Point(7, 1), new Point(9, 4), new Point(10, 8), + new Point(8, 10)), new Point(3, 1), new Point(10, 10))); + } + + private static Stream boxesToTest() { + return Stream.of(Arguments.of(new Box(new Point(1, 1), new Point(5, 5)), new Point(1, 1), new Point(5, 5)), + Arguments.of(new Box(new Point(8, 3), new Point(2, 9)), new Point(2, 3), new Point(8, 9)), + Arguments.of(new Box(new Point(3, 4), new Point(10, 8)), new Point(3, 4), new Point(10, 8))); + } @ParameterizedTest @MethodSource("polygonsToTest") @@ -49,19 +66,4 @@ class BoundingBoxTest { assertThat(boundingBox.getUpperRight()).isEqualTo(ur); } - private static Stream polygonsToTest() { - return Stream.of( - Arguments.of(new Polygon(new Point(1, 1), new Point(5, 1), new Point(5, 5), new Point(5, 1)), new Point(1, 1), - new Point(5, 5)), - Arguments.of(new Polygon(new Point(3, 6), new Point(6, 2), new Point(8, 3), new Point(8, 6), new Point(2, 9)), - new Point(2, 2), new Point(8, 9)), - Arguments.of(new Polygon(new Point(3, 4), new Point(7, 1), new Point(9, 4), new Point(10, 8), new Point(8, 10)), - new Point(3, 1), new Point(10, 10))); - } - - private static Stream boxesToTest() { - return Stream.of(Arguments.of(new Box(new Point(1, 1), new Point(5, 5)), new Point(1, 1), new Point(5, 5)), - Arguments.of(new Box(new Point(8, 3), new Point(2, 9)), new Point(2, 3), new Point(8, 9)), - Arguments.of(new Box(new Point(3, 4), new Point(10, 8)), new Point(3, 4), new Point(10, 8))); - } } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTests.java similarity index 78% rename from src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTests.java index 9be3a1f8c..6407c0cd1 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTests.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import java.time.LocalDateTime; import java.util.Map; @@ -25,16 +22,20 @@ import org.junit.jupiter.api.Test; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Renderer; + import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.integration.shared.common.ScrollingEntity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * @author Michael J. Simons */ -class CypherAdapterUtilsTest { +class CypherAdapterUtilsTests { @Test void shouldCombineSortKeysetProper() { @@ -44,9 +45,10 @@ class CypherAdapterUtilsTest { var n = Constants.NAME_OF_TYPED_ROOT_NODE.apply(entity); var condition = CypherAdapterUtils.combineKeysetIntoCondition(entity, - ScrollPosition.forward(Map.of("foobar", "D0", "b", 3, "c", LocalDateTime.of(2023, 3, 19, 14, 21, 8, 716))), - Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c")), mappingContext.getConversionService() - ); + ScrollPosition + .forward(Map.of("foobar", "D0", "b", 3, "c", LocalDateTime.of(2023, 3, 19, 14, 21, 8, 716))), + Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c")), + mappingContext.getConversionService()); var expected = """ MATCH (scrollingEntity) @@ -60,8 +62,8 @@ class CypherAdapterUtilsTest { AND scrollingEntity.c = $pcdsl03)) RETURN scrollingEntity"""; - assertThat(Renderer.getRenderer(Configuration.prettyPrinting()).render(Cypher.match(Cypher.anyNode(n)).where(condition).returning(n).build())) - .isEqualTo(expected); + assertThat(Renderer.getRenderer(Configuration.prettyPrinting()) + .render(Cypher.match(Cypher.anyNode(n)).where(condition).returning(n).build())).isEqualTo(expected); } @Test @@ -72,8 +74,8 @@ class CypherAdapterUtilsTest { var sortItem = CypherAdapterUtils.sortAdapterFor(entity).apply(Sort.Order.asc("basicComposite.blubb")); var node = Cypher.anyNode("scrollingEntity"); var statement = Cypher.match(node).returning(node).orderBy(sortItem).build(); - assertThat(Renderer.getDefaultRenderer().render(statement)) - .isEqualTo("MATCH (scrollingEntity) RETURN scrollingEntity ORDER BY scrollingEntity.__allProperties__.`basicComposite.blubb`"); + assertThat(Renderer.getDefaultRenderer().render(statement)).isEqualTo( + "MATCH (scrollingEntity) RETURN scrollingEntity ORDER BY scrollingEntity.__allProperties__.`basicComposite.blubb`"); } @Test @@ -81,7 +83,10 @@ class CypherAdapterUtilsTest { var mappingContext = new Neo4jMappingContext(); var entity = mappingContext.getPersistentEntity(ScrollingEntity.class); - assertThatIllegalStateException().isThrownBy(() -> CypherAdapterUtils.sortAdapterFor(entity).apply(Sort.Order.asc("basicComposite"))) - .withMessage("Cannot order by composite property: 'basicComposite'. Only ordering by its nested fields is allowed."); + assertThatIllegalStateException() + .isThrownBy(() -> CypherAdapterUtils.sortAdapterFor(entity).apply(Sort.Order.asc("basicComposite"))) + .withMessage( + "Cannot order by composite property: 'basicComposite'. Only ordering by its nested fields is allowed."); } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/ExtendedTestEntity.java b/src/test/java/org/springframework/data/neo4j/repository/query/ExtendedTestEntity.java index 820c533bc..aeb6a76e8 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/ExtendedTestEntity.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/ExtendedTestEntity.java @@ -21,4 +21,5 @@ package org.springframework.data.neo4j.repository.query; class ExtendedTestEntity extends TestEntity { private String otherAttribute; + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTests.java similarity index 77% rename from src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTests.java index 01e9eb6f3..4e101dfbe 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriterTests.java @@ -15,12 +15,25 @@ */ package org.springframework.data.neo4j.repository.query; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; + import org.springframework.data.annotation.Version; import org.springframework.data.convert.EntityWriter; import org.springframework.data.mapping.MappingException; @@ -41,18 +54,6 @@ import org.springframework.data.neo4j.integration.issues.gh2323.Language; import org.springframework.data.neo4j.integration.issues.gh2323.Person; import org.springframework.util.ReflectionUtils; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.function.Function; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -60,30 +61,25 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Michael J. Simons */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class Neo4jNestedMapEntityWriterTest { +class Neo4jNestedMapEntityWriterTests { private final Neo4jMappingContext mappingContext; + private final Condition isAMap = new Condition<>(Map.class::isInstance, "a map"); + private final Condition isAMapValue = new Condition<>( o -> o instanceof Value && InternalTypeSystem.TYPE_SYSTEM.MAP().isTypeOf((Value) o), "a map value"); + private final Condition isAListValue = new Condition<>( o -> o instanceof Value && InternalTypeSystem.TYPE_SYSTEM.LIST().isTypeOf((Value) o), "a list value"); - Neo4jNestedMapEntityWriterTest() { + Neo4jNestedMapEntityWriterTests() { this.mappingContext = Neo4jMappingContext.builder().withNeo4jConversions(new Neo4jConversions()).build(); this.mappingContext.setInitialEntitySet(new HashSet<>( - Arrays.asList( - FlatEntity.class, - FlatEntityWithAdditionalTypes.class, - FlatEntityWithDynamicLabels.class, - GraphPropertyNamesShouldBeUsed.class, - A.class, B.class, - A2.class, B2.class, - A3.class, A4.class, A5.class, A6.class, A7.class, A8.class, - Person.class, Knows.class, Language.class - ) - )); + Arrays.asList(FlatEntity.class, FlatEntityWithAdditionalTypes.class, FlatEntityWithDynamicLabels.class, + GraphPropertyNamesShouldBeUsed.class, A.class, B.class, A2.class, B2.class, A3.class, A4.class, + A5.class, A6.class, A7.class, A8.class, Person.class, Knows.class, Language.class))); this.mappingContext.initialize(); } @@ -92,10 +88,10 @@ class Neo4jNestedMapEntityWriterTest { FlatEntity entity = new FlatEntity(4711L, 47.11, "4711"); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(new Neo4jMappingContext()); - assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> writer.write(entity, new HashMap<>())) - .withMessageMatching("Cannot write unknown entity of type '.+' into a map"); + EntityWriter> writer = Neo4jNestedMapEntityWriter + .forContext(new Neo4jMappingContext()); + assertThatExceptionOfType(MappingException.class).isThrownBy(() -> writer.write(entity, new HashMap<>())) + .withMessageMatching("Cannot write unknown entity of type '.+' into a map"); } @Test // GH-2323 @@ -109,22 +105,22 @@ class Neo4jNestedMapEntityWriterTest { EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, p); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("Person"); assertThat(result).containsEntry("__id__", Values.value("xxx")); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); - assertThat(properties).hasEntrySatisfying("KNOWS", isAListValue); + assertThat(properties).hasEntrySatisfying("KNOWS", this.isAListValue); List rels = properties.get("KNOWS").asList(Function.identity()); properties = rels.get(0).get("__properties__").asMap(Function.identity()); assertThat(properties).containsEntry("description", Values.value("Some description")); properties = rels.get(0).get("__target__").asMap(Function.identity()); assertThat(properties).containsEntry("__id__", Values.value("German")); - assertThat(properties).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(properties).hasEntrySatisfying("__properties__", this.isAMapValue); } @Test // GH-2323 @@ -138,14 +134,14 @@ class Neo4jNestedMapEntityWriterTest { assertThat(result).containsEntry("__id__", Values.value(4711L)); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(properties).containsEntry("description", Values.value("Some description")); - assertThat(result).hasEntrySatisfying("__target__", isAMapValue); + assertThat(result).hasEntrySatisfying("__target__", this.isAMapValue); Map target = ((Value) result.get("__target__")).asMap(Function.identity()); assertThat(target).containsEntry("__id__", Values.value("German")); - assertThat(target).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(target).hasEntrySatisfying("__properties__", this.isAMapValue); } @Test // DATAGRAPH-1452 @@ -153,14 +149,14 @@ class Neo4jNestedMapEntityWriterTest { FlatEntity entity = new FlatEntity(4711L, 47.11, "4711"); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("FlatEntity"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value(4711L)); assertThat(properties).containsEntry("aDouble", Values.value(47.11)); @@ -171,17 +167,16 @@ class Neo4jNestedMapEntityWriterTest { void additionalTypesShouldWork() { FlatEntityWithAdditionalTypes entity = new FlatEntityWithAdditionalTypes("TheId", 123L, Locale.FRENCH, - URI.create("https://info.michael-simons.eu"), SomeEnum.A, - Collections.singletonList(47.11)); + URI.create("https://info.michael-simons.eu"), SomeEnum.A, Collections.singletonList(47.11)); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("FlatEntityWithAdditionalTypes"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value("TheId")); assertThat(properties).containsEntry("aLocale", Values.value("fr")); @@ -196,15 +191,15 @@ class Neo4jNestedMapEntityWriterTest { FlatEntityWithDynamicLabels entity = new FlatEntityWithDynamicLabels("TheId", Arrays.asList("Label1", "Label2")); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); - assertThat(labels.asList(v -> v.asString())) - .containsExactlyInAnyOrder("FlatEntityWithDynamicLabels", "Label1", "Label2"); + assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("FlatEntityWithDynamicLabels", "Label1", + "Label2"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value("TheId")); assertThat(properties).isEmpty(); @@ -219,24 +214,23 @@ class Neo4jNestedMapEntityWriterTest { A entity = new A("a", b1, Arrays.asList(b2, b3)); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); - assertThat(labels.asList(v -> v.asString())) - .containsExactlyInAnyOrder("A"); + assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value("a")); - assertThat(properties).hasEntrySatisfying("HAS_B", isAListValue); + assertThat(properties).hasEntrySatisfying("HAS_B", this.isAListValue); Map hasBMap = ((Value) properties.get("HAS_B")).get(0).asMap(Function.identity()); assertThat(hasBMap.get("__id__")).isEqualTo(Values.value("bI")); - assertThat(properties).hasEntrySatisfying("HAS_MORE_B", isAListValue); + assertThat(properties).hasEntrySatisfying("HAS_MORE_B", this.isAListValue); List hasMoreBs = ((Value) properties.get("HAS_MORE_B")).asList(Function.identity()); assertThat(hasMoreBs).extracting(v -> v.get("__id__").asString()).containsExactly("bII", "bIII"); } @@ -260,45 +254,44 @@ class Neo4jNestedMapEntityWriterTest { rels2.put("b2rel2", Arrays.asList(b2III)); A3 entity = new A3("a", rels, rels2); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); - assertThat(labels.asList(v -> v.asString())) - .containsExactlyInAnyOrder("A3"); + assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A3"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value("a")); - assertThat(properties).hasEntrySatisfying("brel1a", isAListValue); + assertThat(properties).hasEntrySatisfying("brel1a", this.isAListValue); Map nested = ((Value) properties.get("brel1a")).get(0).asMap(Function.identity()); - assertThat(nested).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(nested).hasEntrySatisfying("__properties__", this.isAMapValue); assertThat(nested.get("__id__")).isEqualTo(Values.value("bI")); - assertThat(properties).hasEntrySatisfying("brel1b", isAListValue); + assertThat(properties).hasEntrySatisfying("brel1b", this.isAListValue); nested = ((Value) properties.get("brel1b")).get(0).asMap(Function.identity()); assertThat(nested.get("__ref__")).isEqualTo(Values.value("bI")); - assertThat(properties).hasEntrySatisfying("brel2", isAListValue); + assertThat(properties).hasEntrySatisfying("brel2", this.isAListValue); nested = ((Value) properties.get("brel2")).get(0).asMap(Function.identity()); - assertThat(nested).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(nested).hasEntrySatisfying("__properties__", this.isAMapValue); assertThat(nested.get("__id__")).isEqualTo(Values.value("bII")); - assertThat(properties).hasEntrySatisfying("b2rel1", isAListValue); + assertThat(properties).hasEntrySatisfying("b2rel1", this.isAListValue); nested = ((Value) properties.get("b2rel1")).get(0).asMap(Function.identity()); - assertThat(nested).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(nested).hasEntrySatisfying("__properties__", this.isAMapValue); assertThat(nested.get("__id__")).isEqualTo(Values.value("b2I")); nested = ((Value) properties.get("b2rel1")).get(1).asMap(Function.identity()); - assertThat(nested).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(nested).hasEntrySatisfying("__properties__", this.isAMapValue); assertThat(nested.get("__id__")).isEqualTo(Values.value("b2II")); - assertThat(properties).hasEntrySatisfying("b2rel2", isAListValue); + assertThat(properties).hasEntrySatisfying("b2rel2", this.isAListValue); nested = ((Value) properties.get("b2rel2")).get(0).asMap(Function.identity()); - assertThat(nested).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(nested).hasEntrySatisfying("__properties__", this.isAMapValue); assertThat(nested.get("__id__")).isEqualTo(Values.value("b2III")); } @@ -312,35 +305,34 @@ class Neo4jNestedMapEntityWriterTest { A2 a2 = new A2("a2II", new B2("b2II")); b1.knows = Arrays.asList(entity, a2); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, entity); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); - assertThat(labels.asList(v -> v.asString())) - .containsExactlyInAnyOrder("A2"); + assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A2"); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); assertThat(result).containsEntry("__id__", Values.value("a2I")); - assertThat(properties).hasEntrySatisfying("HAS_B", isAListValue); + assertThat(properties).hasEntrySatisfying("HAS_B", this.isAListValue); List hasBs = ((Value) properties.get("HAS_B")).asList(Function.identity()); assertThat(hasBs).hasSize(1); Map hasBMap = hasBs.get(0).asMap(Function.identity()); assertThat(hasBMap).containsEntry("__id__", Values.value("b2I")); - assertThat(hasBMap).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(hasBMap).hasEntrySatisfying("__properties__", this.isAMapValue); properties = hasBMap.get("__properties__").asMap(Function.identity()); - assertThat(properties).hasEntrySatisfying("KNOWS", isAListValue); + assertThat(properties).hasEntrySatisfying("KNOWS", this.isAListValue); List rels = ((Value) properties.get("KNOWS")).asList(Function.identity()); assertThat(rels).first().satisfies(v -> assertThat(v.get("__ref__").asString()).isEqualTo("a2I")); assertThat(rels).last().satisfies(v -> { Map nestedProperties = v.get("__properties__").asMap(Function.identity()); assertThat(nestedProperties).containsOnlyKeys("HAS_B"); assertThat(nestedProperties.get("HAS_B").asList(Function.identity()).get(0).asMap(Function.identity())) - .doesNotContainKey("KNOWS"); + .doesNotContainKey("KNOWS"); }); } @@ -351,19 +343,19 @@ class Neo4jNestedMapEntityWriterTest { final B3 b = new B3("b3I"); a.p1 = new P1("v1", "v2", b); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, a); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A4"); assertThat(result).containsEntry("__id__", Values.value("a4I")); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); - assertThat(properties).hasEntrySatisfying("HAS_P1", isAListValue); + assertThat(properties).hasEntrySatisfying("HAS_P1", this.isAListValue); List rels = properties.get("HAS_P1").asList(Function.identity()); properties = rels.get(0).get("__properties__").asMap(Function.identity()); @@ -372,31 +364,28 @@ class Neo4jNestedMapEntityWriterTest { properties = rels.get(0).get("__target__").asMap(Function.identity()); assertThat(properties).containsEntry("__id__", Values.value("b3I")); - assertThat(properties).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(properties).hasEntrySatisfying("__properties__", this.isAMapValue); } @Test // DATAGRAPH-1452 void oneToManyRelationshipWithPropertiesShouldWork() { final A5 a = new A5("a5I"); - a.p1 = Arrays.asList( - new P1("v0", "v1", new B3("b3I")), - new P1("v2", "v3", new B3("b3II")) - ); + a.p1 = Arrays.asList(new P1("v0", "v1", new B3("b3I")), new P1("v2", "v3", new B3("b3II"))); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, a); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A5"); assertThat(result).containsEntry("__id__", Values.value("a5I")); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); - assertThat(properties).hasEntrySatisfying("HAS_P1", isAListValue); + assertThat(properties).hasEntrySatisfying("HAS_P1", this.isAListValue); List rels = properties.get("HAS_P1").asList(Function.identity()); String[] suffix = new String[] { "I", "II" }; for (int i = 0; i < suffix.length; ++i) { @@ -406,7 +395,7 @@ class Neo4jNestedMapEntityWriterTest { properties = rels.get(i).get("__target__").asMap(Function.identity()); assertThat(properties).containsEntry("__id__", Values.value("b3" + suffix[i])); - assertThat(properties).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(properties).hasEntrySatisfying("__properties__", this.isAMapValue); } } @@ -418,23 +407,23 @@ class Neo4jNestedMapEntityWriterTest { a.p1.put("rel1", new P1("v0", "v1", new B3("b3I"))); a.p1.put("rel2", new P1("v2", "v3", new B3("b3II"))); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, a); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A6"); assertThat(result).containsEntry("__id__", Values.value("a6I")); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); String[] rels = new String[] { "rel1", "rel2" }; for (int i = 0; i < rels.length; i++) { String rel = rels[i]; - assertThat(properties).hasEntrySatisfying(rel, isAListValue); + assertThat(properties).hasEntrySatisfying(rel, this.isAListValue); List relValue = properties.get(rel).asList(Function.identity()); assertThat(relValue).hasSize(1); @@ -450,25 +439,22 @@ class Neo4jNestedMapEntityWriterTest { final A7 a = new A7("a7I"); a.p1 = new HashMap<>(); a.p1.put("rel1", Arrays.asList(new P1("v0", "v1", new B3("b3I")))); - a.p1.put("rel2", Arrays.asList( - new P1("v2", "v3", new B3("b3II")), - new P1("v4", "v5", new B3("b3III")) - )); + a.p1.put("rel2", Arrays.asList(new P1("v2", "v3", new B3("b3II")), new P1("v4", "v5", new B3("b3III")))); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, a); - assertThat(result).hasEntrySatisfying("__labels__", isAListValue); + assertThat(result).hasEntrySatisfying("__labels__", this.isAListValue); Value labels = (Value) result.get("__labels__"); assertThat(labels.asList(v -> v.asString())).containsExactlyInAnyOrder("A7"); assertThat(result).containsEntry("__id__", Values.value("a7I")); - assertThat(result).hasEntrySatisfying("__properties__", isAMap); + assertThat(result).hasEntrySatisfying("__properties__", this.isAMap); Map properties = (Map) result.get("__properties__"); - assertThat(properties).hasEntrySatisfying("rel1", isAListValue); - assertThat(properties).hasEntrySatisfying("rel2", isAListValue); + assertThat(properties).hasEntrySatisfying("rel1", this.isAListValue); + assertThat(properties).hasEntrySatisfying("rel2", this.isAListValue); List rels = new ArrayList<>(); rels.addAll(properties.get("rel1").asList(Function.identity())); rels.addAll(properties.get("rel2").asList(Function.identity())); @@ -480,7 +466,7 @@ class Neo4jNestedMapEntityWriterTest { properties = rels.get(i).get("__target__").asMap(Function.identity()); assertThat(properties).containsEntry("__id__", Values.value("b3" + suffix[i])); - assertThat(properties).hasEntrySatisfying("__properties__", isAMapValue); + assertThat(properties).hasEntrySatisfying("__properties__", this.isAMapValue); } } @@ -493,13 +479,13 @@ class Neo4jNestedMapEntityWriterTest { a.b2 = new B2("b2I"); a.b3 = Arrays.asList(new B3("b3I"), new B3("b3II")); - EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(mappingContext); + EntityWriter> writer = Neo4jNestedMapEntityWriter.forContext(this.mappingContext); Map result = toMap(writer, a); assertThat(((Map) result.get("__properties__")).get("HAS").size()).isEqualTo(4); } - public Map toMap(EntityWriter> writer, Object source) { + Map toMap(EntityWriter> writer, Object source) { if (source == null) { return Collections.emptyMap(); @@ -510,6 +496,12 @@ class Neo4jNestedMapEntityWriterTest { return result; } + enum SomeEnum { + + A, B + + } + @Node static class FlatEntity { @@ -526,6 +518,7 @@ class Neo4jNestedMapEntityWriterTest { this.aDouble = aDouble; this.aString = aString; } + } @Node @@ -545,7 +538,8 @@ class Neo4jNestedMapEntityWriterTest { private List listOfDoubles; - FlatEntityWithAdditionalTypes(String id, Long version, Locale aLocale, URI aURI, SomeEnum someEnum, List listOfDoubles) { + FlatEntityWithAdditionalTypes(String id, Long version, Locale aLocale, URI aURI, SomeEnum someEnum, + List listOfDoubles) { this.id = id; this.version = version; this.aLocale = aLocale; @@ -553,9 +547,8 @@ class Neo4jNestedMapEntityWriterTest { this.someEnum = someEnum; this.listOfDoubles = listOfDoubles; } - } - enum SomeEnum { A, B } + } static class SomeIdGeneratory implements IdGenerator { @@ -563,6 +556,7 @@ class Neo4jNestedMapEntityWriterTest { public String generateId(String primaryLabel, Object entity) { return "abc"; } + } @Node @@ -579,6 +573,7 @@ class Neo4jNestedMapEntityWriterTest { this.id = id; this.dynamicLabels = dynamicLabels; } + } @Node @@ -595,6 +590,7 @@ class Neo4jNestedMapEntityWriterTest { this.id = id; this.aField = aField; } + } @Node @@ -613,6 +609,7 @@ class Neo4jNestedMapEntityWriterTest { this.hasB = hasB; this.hasMoreBs = hasMoreBs; } + } @Node @@ -624,6 +621,7 @@ class Neo4jNestedMapEntityWriterTest { B(String id) { this.id = id; } + } @Node @@ -638,6 +636,7 @@ class Neo4jNestedMapEntityWriterTest { this.id = id; this.hasB = hasB; } + } @Node @@ -653,6 +652,7 @@ class Neo4jNestedMapEntityWriterTest { B2(String id) { this.id = id; } + } @Node @@ -672,6 +672,7 @@ class Neo4jNestedMapEntityWriterTest { this.hasB = hasB; this.hasMoreBs = hasMoreBs; } + } @Node @@ -686,14 +687,12 @@ class Neo4jNestedMapEntityWriterTest { A4(String id) { this.id = id; } + } @RelationshipProperties static class P1 { - @RelationshipId - private Long id; - private final String prop1; private final String prop2; @@ -701,6 +700,9 @@ class Neo4jNestedMapEntityWriterTest { @TargetNode private final B3 b3; + @RelationshipId + private Long id; + P1(String prop1, String prop2, B3 b3) { this.prop1 = prop1; this.prop2 = prop2; @@ -714,17 +716,15 @@ class Neo4jNestedMapEntityWriterTest { this.b3 = b3; } - @Override - public String toString() { - return "P1{" + - "prop1='" + prop1 + '\'' + - ", prop2='" + prop2 + '\'' + - '}'; + P1 withId(Long newId) { + return (this.id != newId) ? new P1(newId, this.prop1, this.prop2, this.b3) : this; } - P1 withId(Long newId) { - return this.id == newId ? this : new P1(newId, this.prop1, this.prop2, this.b3); + @Override + public String toString() { + return "P1{" + "prop1='" + this.prop1 + '\'' + ", prop2='" + this.prop2 + '\'' + '}'; } + } @Node @@ -736,6 +736,7 @@ class Neo4jNestedMapEntityWriterTest { B3(String id) { this.id = id; } + } @Node @@ -750,6 +751,7 @@ class Neo4jNestedMapEntityWriterTest { A5(String id) { this.id = id; } + } @Node @@ -764,6 +766,7 @@ class Neo4jNestedMapEntityWriterTest { A6(String id) { this.id = id; } + } @Node @@ -778,6 +781,7 @@ class Neo4jNestedMapEntityWriterTest { A7(String id) { this.id = id; } + } @Node @@ -798,5 +802,7 @@ class Neo4jNestedMapEntityWriterTest { A8(String id) { this.id = id; } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTests.java similarity index 86% rename from src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTests.java index 3b9f3c3f5..848c5703d 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTests.java @@ -15,10 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assumptions.assumeThat; - import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -37,6 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueExpressionParser; @@ -49,11 +46,14 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.ReflectionUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assumptions.assumeThat; + /** * @author Michael J. Simons - * @soundtrack Red Hot Chili Peppers - Californication */ -class Neo4jSpelSupportTest { +class Neo4jSpelSupportTests { @Test // DATAGRAPH-1454 void literalOfShouldWork() { @@ -88,12 +88,12 @@ class Neo4jSpelSupportTest { assertThat(literalReplacement.getValue()).isEqualTo(""); assertThatIllegalArgumentException().isThrownBy(() -> Neo4jSpelSupport.orderBy("a lizard")) - .withMessageMatching(".+is not a valid order criteria"); + .withMessageMatching(".+is not a valid order criteria"); } private Map getCacheInstance() throws ClassNotFoundException, IllegalAccessException { - Class type = Class.forName( - "org.springframework.data.neo4j.repository.query.Neo4jSpelSupport$StringBasedLiteralReplacement"); + Class type = Class + .forName("org.springframework.data.neo4j.repository.query.Neo4jSpelSupport$StringBasedLiteralReplacement"); Field cacheField = ReflectionUtils.findField(type, "INSTANCES"); cacheField.setAccessible(true); return (Map) cacheField.get(null); @@ -103,8 +103,9 @@ class Neo4jSpelSupportTest { try { Map cache = getCacheInstance(); cache.clear(); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } @@ -112,8 +113,9 @@ class Neo4jSpelSupportTest { try { Map cache = getCacheInstance(); return cache.size(); - } catch (Exception e) { - throw new RuntimeException(e); + } + catch (Exception ex) { + throw new RuntimeException(ex); } } @@ -165,10 +167,8 @@ class Neo4jSpelSupportTest { } @ParameterizedTest // GH-2279 - @CsvSource({ - "MATCH (n:Something) WHERE n.name = ?#{#name}, MATCH (n:Something) WHERE n.name = ?__HASH__{#name}", - "MATCH (n:Something) WHERE n.name = :#{#name}, MATCH (n:Something) WHERE n.name = :__HASH__{#name}" - }) + @CsvSource({ "MATCH (n:Something) WHERE n.name = ?#{#name}, MATCH (n:Something) WHERE n.name = ?__HASH__{#name}", + "MATCH (n:Something) WHERE n.name = :#{#name}, MATCH (n:Something) WHERE n.name = :__HASH__{#name}" }) void shouldQuoteParameterExpressionsCorrectly(String query, String expected) { String quoted = Neo4jSpelSupport.potentiallyQuoteExpressionsParameter(query); @@ -176,10 +176,8 @@ class Neo4jSpelSupportTest { } @ParameterizedTest // GH-2279 - @CsvSource({ - "MATCH (n:Something) WHERE n.name = ?__HASH__{#name}, MATCH (n:Something) WHERE n.name = ?#{#name}", - "MATCH (n:Something) WHERE n.name = :__HASH__{#name}, MATCH (n:Something) WHERE n.name = :#{#name}" - }) + @CsvSource({ "MATCH (n:Something) WHERE n.name = ?__HASH__{#name}, MATCH (n:Something) WHERE n.name = ?#{#name}", + "MATCH (n:Something) WHERE n.name = :__HASH__{#name}, MATCH (n:Something) WHERE n.name = :#{#name}" }) void shouldUnquoteParameterExpressionsCorrectly(String quoted, String expected) { String query = Neo4jSpelSupport.potentiallyUnquoteParameterExpressions(quoted); @@ -194,6 +192,7 @@ class Neo4jSpelSupportTest { ValueExpressionQueryRewriter.ParsedQuery spelExtractor; class R implements LiteralReplacement { + private final String value; R(String value) { @@ -202,13 +201,14 @@ class Neo4jSpelSupportTest { @Override public String getValue() { - return value; + return this.value; } @Override public Target getTarget() { return Target.UNSPECIFIED; } + } Map parameters = new HashMap<>(); @@ -217,8 +217,10 @@ class Neo4jSpelSupportTest { parameters.put("__SpEL__" + i, new R("'x" + i + "'")); } template.delete(template.length() - 4, template.length()); - spelExtractor = ValueExpressionQueryRewriter.of(ValueExpressionDelegate.create(), - StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource).parse(template.toString()); + spelExtractor = ValueExpressionQueryRewriter + .of(ValueExpressionDelegate.create(), StringBasedNeo4jQuery::parameterNameSource, + StringBasedNeo4jQuery::replacementSource) + .parse(template.toString()); query = spelExtractor.getQueryString(); Neo4jQuerySupport.QueryContext qc = new Neo4jQuerySupport.QueryContext("n/a", query, parameters); assertThat(qc.query).isEqualTo( @@ -228,7 +230,8 @@ class Neo4jSpelSupportTest { @Test // GH-2279 void shouldQuoteParameterExpressionsCorrectly() { - String quoted = Neo4jSpelSupport.potentiallyQuoteExpressionsParameter("MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name}"); + String quoted = Neo4jSpelSupport + .potentiallyQuoteExpressionsParameter("MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name}"); assertThat(quoted).isEqualTo("MATCH (n:#{#staticLabels}) WHERE n.name = ?__HASH__{#name}"); } @@ -240,14 +243,20 @@ class Neo4jSpelSupportTest { String query = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery( "MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n", - new Neo4jMappingContext(), (EntityMetadata) () -> BikeNode.class, ValueExpressionParser.create()); + new Neo4jMappingContext(), (EntityMetadata) () -> BikeNode.class, + ValueExpressionParser.create()); - assertThat(query).isEqualTo("MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n"); + assertThat(query).isEqualTo( + "MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n"); } - @Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"}) + @Node(primaryLabel = "Bike", labels = { "Gravel", "Easy Trail" }) static class BikeNode { - @Id String id; + + @Id + String id; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTests.java similarity index 57% rename from src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTests.java index 74d699121..805245dc3 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTests.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.spy; - import java.lang.reflect.Method; import java.util.Collections; import java.util.List; @@ -33,9 +27,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.domain.PageRequest; @@ -59,8 +57,10 @@ import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.ReflectionUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.spy; /** * Unit tests for @@ -69,10 +69,9 @@ import reactor.core.publisher.Mono; * * * @author Michael J. Simons - * @soundtrack Red Hot Chili Peppers - Stadium Arcadium */ @ExtendWith(MockitoExtension.class) -final class ReactiveRepositoryQueryTest { +final class ReactiveRepositoryQueryTests { private static final RepositoryMetadata TEST_REPOSITORY_METADATA = new DefaultRepositoryMetadata( TestRepository.class); @@ -88,196 +87,12 @@ final class ReactiveRepositoryQueryTest { @Mock private ProjectionFactory projectionFactory; - @Nested - @ExtendWith(LogbackCapturingExtension.class) - class ReactiveStringBasedNeo4jQueryTest { - - @Test - void spelQueryContextShouldBeConfiguredCorrectly() { - - ValueExpressionQueryRewriter rewriter = ReactiveStringBasedNeo4jQuery.createQueryRewriter(ValueExpressionDelegate.create()); - - String template; - String query; - ValueExpressionQueryRewriter.ParsedQuery parsedQuery; - - template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; - - parsedQuery = rewriter.parse(template); - query = parsedQuery.getQueryString(); - - assertThat(query) - .isEqualTo( - "MATCH (user:User) WHERE user.name = $__SpEL__0 and user.middleName = $__SpEL__1 RETURN user"); - - template = "MATCH (user:User) WHERE user.name=?#{[0]} and user.name=:#{[0]} RETURN user"; - parsedQuery = rewriter.parse(template); - query = parsedQuery.getQueryString(); - - assertThat(query) - .isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user"); - } - - @Test - void shouldDetectInvalidAnnotation() { - - Neo4jQueryMethod method = reactiveNeo4jQueryMethod("annotatedQueryWithoutTemplate"); - assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> ReactiveStringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)) - .withMessage("Expected @Query annotation to have a value, but it did not"); - } - - @Test // DATAGRAPH-1440 - void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = reactiveNeo4jQueryMethod("findAllExtendedEntitiesWithCustomQuery", Sort.class); - ReactiveStringBasedNeo4jQuery query = - ReactiveStringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { Sort.by("name").ascending() }); - - query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - ".*" + Pattern - .quote("Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") - + ".*")) - .anyMatch(s -> s.matches( - "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); - } - - @Test // DATAGRAPH-1454 - void orderBySpelShouldWork(LogbackCapture logbackCapture) { - - ConfigurableApplicationContext context = new GenericApplicationContext(); - context.getBeanFactory().registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), - new Neo4jEvaluationContextExtension()); - context.refresh(); - - Neo4jQueryMethod method = reactiveNeo4jQueryMethod("orderBySpel", Pageable.class); - ValueExpressionDelegate delegate = getValueExpressionDelegate(context); - ReactiveStringBasedNeo4jQuery query = - ReactiveStringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, delegate, method, projectionFactory); - - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); - PreparedQuery pq = query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()) - .isEqualTo("MATCH (n:Test) RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); - assertThat(logbackCapture.getFormattedMessages()) - .noneMatch(s -> s.matches( - ".*Please specify the order in the query itself and use an unsorted page request\\..*")) - .noneMatch(s -> s.matches( - "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); - } - - @Test // DATAGRAPH-1454 - void literalReplacementsShouldWork() { - - ConfigurableApplicationContext context = new GenericApplicationContext(); - context.getBeanFactory().registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), - new Neo4jEvaluationContextExtension()); - context.refresh(); - - Neo4jQueryMethod method = reactiveNeo4jQueryMethod("makeStaticThingsDynamic", String.class, String.class, - String.class, String.class, Sort.class); - ReactiveStringBasedNeo4jQuery query = - ReactiveStringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - getValueExpressionDelegate(context), - method, projectionFactory); - - String s = Mono.fromSupplier(() -> { - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { "A valid ", "dynamic Label", "dyn prop", "static value", - Sort.by("name").ascending() }); - PreparedQuery pq = query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - return pq.getQueryFragmentsAndParameters().getCypherQuery(); - }).block(); - assertThat(s) - .isEqualTo( - "MATCH (n:`A valid dynamic Label`) SET n.`dyn prop` = 'static value' RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); - } - - @Test - void shouldBindParameters() { - - Neo4jQueryMethod method = reactiveNeo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, - String.class); - - ReactiveStringBasedNeo4jQuery repositoryQuery = spy( - ReactiveStringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), - method, projectionFactory)); - - // skip conversion - doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); - - Map resolveParameters = repositoryQuery.bindParameters(new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { "A String", "Another String" })); - - assertThat(resolveParameters).containsEntry("0", "A String").containsEntry("1", "Another String"); - } - - @Test - void shouldResolveNamedParameters() { - - Neo4jQueryMethod method = ReactiveRepositoryQueryTest - .reactiveNeo4jQueryMethod("findByDontDoThisInRealLiveNamed", - Point.class, String.class, String.class); - - ReactiveStringBasedNeo4jQuery repositoryQuery = spy( - ReactiveStringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), - method, projectionFactory)); - - // skip conversion - doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); - - Point thePoint = Values.point(4223, 1, 2).asPoint(); - Map resolveParameters = repositoryQuery - .bindParameters( - new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { thePoint, "TheName", "TheFirstName" })); - - assertThat(resolveParameters).hasSize(8).containsEntry("0", thePoint).containsEntry("location", thePoint) - .containsEntry("1", "TheName").containsEntry("name", "TheName").containsEntry("2", "TheFirstName") - .containsEntry("firstName", "TheFirstName").containsEntry("__SpEL__0", "TheFirstName") - .containsEntry("__SpEL__1", "TheNameTheFirstName"); - } + private ReactiveRepositoryQueryTests() { } private static ValueExpressionDelegate getValueExpressionDelegate(ConfigurableApplicationContext context) { - QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(context.getEnvironment(), context.getBeanFactory()); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( + context.getEnvironment(), context.getBeanFactory()); ValueExpressionDelegate delegate = new ValueExpressionDelegate(accessor, ValueExpressionDelegate.create()); return delegate; } @@ -289,8 +104,8 @@ final class ReactiveRepositoryQueryTest { private static ReactiveNeo4jQueryMethod reactiveNeo4jQueryMethod(String name, Class... parameters) { - return new ReactiveNeo4jQueryMethod(queryMethod(name, parameters), - TEST_REPOSITORY_METADATA, PROJECTION_FACTORY); + return new ReactiveNeo4jQueryMethod(queryMethod(name, parameters), TEST_REPOSITORY_METADATA, + PROJECTION_FACTORY); } private interface TestRepository extends ReactiveCrudRepository { @@ -298,19 +113,14 @@ final class ReactiveRepositoryQueryTest { @Query("MATCH (n:Test) WHERE n.name = $0 OR n.name = $1") Flux annotatedQueryWithValidTemplate(String name, String anotherName); - @Query(value = "MATCH (n:`:#{literal(#aDynamicLabelPt1 + #aDynamicLabelPt2)}`) " - + "SET n.`:#{literal(#aDynamicProperty)}` = :#{literal('''' + #enforcedLiteralValue + '''')} " - + "RETURN n :#{orderBy(#sort)} SKIP $skip LIMIT $limit" - ) - Flux makeStaticThingsDynamic( - @Param("aDynamicLabelPt1") String aDynamicLabelPt1, - @Param("aDynamicLabelPt2") String aDynamicLabelPt2, - @Param("aDynamicProperty") String aDynamicProperty, - @Param("enforcedLiteralValue") String enforcedLiteralValue, - Sort sort - ); + @Query("MATCH (n:`:#{literal(#aDynamicLabelPt1 + #aDynamicLabelPt2)}`) " + + "SET n.`:#{literal(#aDynamicProperty)}` = :#{literal('''' + #enforcedLiteralValue + '''')} " + + "RETURN n :#{orderBy(#sort)} SKIP $skip LIMIT $limit") + Flux makeStaticThingsDynamic(@Param("aDynamicLabelPt1") String aDynamicLabelPt1, + @Param("aDynamicLabelPt2") String aDynamicLabelPt2, @Param("aDynamicProperty") String aDynamicProperty, + @Param("enforcedLiteralValue") String enforcedLiteralValue, Sort sort); - @Query(value = "MATCH (n:Test) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit") + @Query("MATCH (n:Test) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit") Flux orderBySpel(Pageable page); @Query @@ -322,8 +132,184 @@ final class ReactiveRepositoryQueryTest { @Query("MATCH (n:Test) WHERE n.name = $name AND n.firstName = :#{#firstName} AND n.fullName = ?#{#name + #firstName} AND p.location = $location return n") Mono findByDontDoThisInRealLiveNamed(@Param("location") org.neo4j.driver.types.Point location, @Param("name") String name, @Param("firstName") String aFirstName); + } - private ReactiveRepositoryQueryTest() { + @Nested + @ExtendWith(LogbackCapturingExtension.class) + class ReactiveStringBasedNeo4jQueryTest { + + @Test + void spelQueryContextShouldBeConfiguredCorrectly() { + + ValueExpressionQueryRewriter rewriter = ReactiveStringBasedNeo4jQuery + .createQueryRewriter(ValueExpressionDelegate.create()); + + String template; + String query; + ValueExpressionQueryRewriter.ParsedQuery parsedQuery; + + template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; + + parsedQuery = rewriter.parse(template); + query = parsedQuery.getQueryString(); + + assertThat(query).isEqualTo( + "MATCH (user:User) WHERE user.name = $__SpEL__0 and user.middleName = $__SpEL__1 RETURN user"); + + template = "MATCH (user:User) WHERE user.name=?#{[0]} and user.name=:#{[0]} RETURN user"; + parsedQuery = rewriter.parse(template); + query = parsedQuery.getQueryString(); + + assertThat(query) + .isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user"); + } + + @Test + void shouldDetectInvalidAnnotation() { + + Neo4jQueryMethod method = reactiveNeo4jQueryMethod("annotatedQueryWithoutTemplate"); + assertThatExceptionOfType(MappingException.class) + .isThrownBy( + () -> ReactiveStringBasedNeo4jQuery.create(ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), + method, ReactiveRepositoryQueryTests.this.projectionFactory)) + .withMessage("Expected @Query annotation to have a value, but it did not"); + } + + @Test // DATAGRAPH-1440 + void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = reactiveNeo4jQueryMethod("findAllExtendedEntitiesWithCustomQuery", Sort.class); + ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery.create( + ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + ReactiveRepositoryQueryTests.this.projectionFactory); + + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { Sort.by("name").ascending() }); + + query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, Neo4jQueryType.DEFAULT, + () -> (typeSystem, mapAccessor) -> new TestEntity(), UnaryOperator.identity()); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches(".*" + Pattern.quote( + "Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") + + ".*")) + .anyMatch(s -> s.matches( + "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); + } + + @Test // DATAGRAPH-1454 + void orderBySpelShouldWork(LogbackCapture logbackCapture) { + + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory() + .registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), + new Neo4jEvaluationContextExtension()); + context.refresh(); + + Neo4jQueryMethod method = reactiveNeo4jQueryMethod("orderBySpel", Pageable.class); + ValueExpressionDelegate delegate = getValueExpressionDelegate(context); + ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery.create( + ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, delegate, method, + ReactiveRepositoryQueryTests.this.projectionFactory); + + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); + PreparedQuery pq = query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, + Neo4jQueryType.DEFAULT, () -> (typeSystem, mapAccessor) -> new TestEntity(), + UnaryOperator.identity()); + assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()) + .isEqualTo("MATCH (n:Test) RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); + assertThat(logbackCapture.getFormattedMessages()) + .noneMatch(s -> s + .matches(".*Please specify the order in the query itself and use an unsorted page request\\..*")) + .noneMatch(s -> s.matches( + "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); + } + + @Test // DATAGRAPH-1454 + void literalReplacementsShouldWork() { + + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory() + .registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), + new Neo4jEvaluationContextExtension()); + context.refresh(); + + Neo4jQueryMethod method = reactiveNeo4jQueryMethod("makeStaticThingsDynamic", String.class, String.class, + String.class, String.class, Sort.class); + ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery.create( + ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, getValueExpressionDelegate(context), method, + ReactiveRepositoryQueryTests.this.projectionFactory); + + String s = Mono.fromSupplier(() -> { + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), new Object[] { "A valid ", + "dynamic Label", "dyn prop", "static value", Sort.by("name").ascending() }); + PreparedQuery pq = query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, + Neo4jQueryType.DEFAULT, () -> (typeSystem, mapAccessor) -> new TestEntity(), + UnaryOperator.identity()); + return pq.getQueryFragmentsAndParameters().getCypherQuery(); + }).block(); + assertThat(s).isEqualTo( + "MATCH (n:`A valid dynamic Label`) SET n.`dyn prop` = 'static value' RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); + } + + @Test + void shouldBindParameters() { + + Neo4jQueryMethod method = reactiveNeo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, + String.class); + + ReactiveStringBasedNeo4jQuery repositoryQuery = spy( + ReactiveStringBasedNeo4jQuery.create(ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), + method, ReactiveRepositoryQueryTests.this.projectionFactory)); + + // skip conversion + Mockito.doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); + + Map resolveParameters = repositoryQuery + .bindParameters(new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { "A String", "Another String" })); + + assertThat(resolveParameters).containsEntry("0", "A String").containsEntry("1", "Another String"); + } + + @Test + void shouldResolveNamedParameters() { + + Neo4jQueryMethod method = ReactiveRepositoryQueryTests + .reactiveNeo4jQueryMethod("findByDontDoThisInRealLiveNamed", Point.class, String.class, String.class); + + ReactiveStringBasedNeo4jQuery repositoryQuery = spy( + ReactiveStringBasedNeo4jQuery.create(ReactiveRepositoryQueryTests.this.neo4jOperations, + ReactiveRepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), + method, ReactiveRepositoryQueryTests.this.projectionFactory)); + + // skip conversion + Mockito.doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); + + Point thePoint = Values.point(4223, 1, 2).asPoint(); + Map resolveParameters = repositoryQuery + .bindParameters(new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { thePoint, "TheName", "TheFirstName" })); + + assertThat(resolveParameters).hasSize(8) + .containsEntry("0", thePoint) + .containsEntry("location", thePoint) + .containsEntry("1", "TheName") + .containsEntry("name", "TheName") + .containsEntry("2", "TheFirstName") + .containsEntry("firstName", "TheFirstName") + .containsEntry("__SpEL__0", "TheFirstName") + .containsEntry("__SpEL__1", "TheNameTheFirstName"); + } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTests.java similarity index 56% rename from src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java rename to src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTests.java index 41eb04826..c9a28c5e4 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTests.java @@ -15,16 +15,6 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import reactor.core.publisher.Mono; - import java.lang.reflect.Method; import java.util.Collections; import java.util.List; @@ -43,11 +33,14 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Answers; +import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; +import reactor.core.publisher.Mono; + import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -80,6 +73,13 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.ReflectionUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.spy; + /** * Unit tests for *
    @@ -91,7 +91,7 @@ import org.springframework.util.ReflectionUtils; * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -final class RepositoryQueryTest { +final class RepositoryQueryTests { private static final String CUSTOM_CYPHER_QUERY = "MATCH (n) return n"; @@ -100,6 +100,9 @@ final class RepositoryQueryTest { private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory(); + @Mock + NamedQueries namedQueries; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Neo4jMappingContext neo4jMappingContext; @@ -109,426 +112,7 @@ final class RepositoryQueryTest { @Mock private ProjectionFactory projectionFactory; - @ParameterizedTest - @ValueSource(strings = { - "RETURN 1 SKIP $skip LIMIT $limit", - "RETURN 1 sKip $skip limit $limit", - "match(n) return $\n" - + " skip \n" - + " skip $\n" - + " skip \n" - + " LIMIT $ limit", - "MATCH (n) RETURN n Skip $skip LIMIT /* No */ $ " - + " /* NO */ limit", - "MATCH (n) RETURN n Skip $skip LIMIT /* No */$/* NO */ limit", - "MATCH (n) RETURN n Skip $skip LIMIT // No, no\n" - + " $ /* really, not a */ limit" - }) - void shouldDetectValidSkipAndLimitPlaceholders(String template) { - - assertThat(StringBasedNeo4jQuery.hasSkipAndLimitKeywordsAndPlaceholders(template)).isTrue(); - } - - @ParameterizedTest - @ValueSource(strings = { - "RETURN 1 SKIP $SKIP LIMIT $LIMIT", - "RETURN 1 skip $skiP limit $lImit", - "RETURN 1 skip // $skiP limit $lImit", - "RETURN 1 skip $skiP limit // $lImit", - }) - void shouldDetectValidSkipAndLimitPlaceholdersNegative(String template) { - - assertThat(StringBasedNeo4jQuery.hasSkipAndLimitKeywordsAndPlaceholders(template)).isFalse(); - } - - @Mock NamedQueries namedQueries; - - @Nested - class Neo4jQueryMethodTest { - - @Test - void findQueryAnnotation() { - - Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("annotatedQueryWithValidTemplate"); - - Optional optionalQueryAnnotation = neo4jQueryMethod.getQueryAnnotation(); - assertThat(optionalQueryAnnotation).isPresent(); - } - - @Test - void streamQueriesShouldBeTreatedAsCollectionQueries() { - - Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("findAllByIdGreaterThan", long.class); - - assumeThat(neo4jQueryMethod.isStreamQuery()).isTrue(); - assertThat(neo4jQueryMethod.isCollectionLikeQuery()).isTrue(); - } - - @Test - void collectionQueriesShouldBeTreatedAsSuch() { - - Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("findAllByANamedQuery"); - - assumeThat(neo4jQueryMethod.isCollectionQuery()).isTrue(); - assertThat(neo4jQueryMethod.isCollectionLikeQuery()).isTrue(); - } - - @Test - void shouldFailOnMonoOfPageAsReturnType() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> reactiveNeo4jQueryMethod("findAllByName", String.class, Pageable.class)); - } - - @Test - void shouldFailForPageableParameterOnMonoOfPageAsReturnType() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> reactiveNeo4jQueryMethod("findAllByName", String.class, Pageable.class)); - } - - @Test - void shouldFailForPageableParameterOnMonoOfSliceAsReturnType() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy( - () -> reactiveNeo4jQueryMethod("findAllByNameStartingWith", String.class, Pageable.class)); - } - } - - @Nested - class Neo4jQueryLookupStrategyTest { - - @Test - void shouldSelectPartTreeNeo4jQuery() { - - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, - neo4jMappingContext, - ValueExpressionDelegate.create(), Configuration.defaultConfig()); - - RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("findById", Object.class), - TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, namedQueries); - assertThat(query).isInstanceOf(PartTreeNeo4jQuery.class); - } - - @Test - void shouldSelectStringBasedNeo4jQuery() { - - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), Configuration.defaultConfig()); - - RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("annotatedQueryWithValidTemplate"), - TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, namedQueries); - assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); - } - - @Test - void shouldSelectStringBasedNeo4jQueryForNamedQuery() { - - final String namedQueryName = "TestEntity.findAllByANamedQuery"; - when(namedQueries.hasQuery(namedQueryName)).thenReturn(true); - when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n"); - - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), Configuration.defaultConfig()); - - RepositoryQuery query = lookupStrategy - .resolveQuery(queryMethod("findAllByANamedQuery"), TEST_REPOSITORY_METADATA, - PROJECTION_FACTORY, namedQueries); - assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); - } - } - - @Nested - @ExtendWith(LogbackCapturingExtension.class) - class StringBasedNeo4jQueryTest { - - @Test - void spelQueryContextShouldBeConfiguredCorrectly() { - ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter spelQueryContext = ValueExpressionQueryRewriter.of( - ValueExpressionDelegate.create(), StringBasedNeo4jQuery::parameterNameSource, - StringBasedNeo4jQuery::replacementSource); - - String template; - String query; - ValueExpressionQueryRewriter.ParsedQuery spelExtractor; - - template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; - - spelExtractor = spelQueryContext.parse(template); - query = spelExtractor.getQueryString(); - - assertThat(query) - .isEqualTo( - "MATCH (user:User) WHERE user.name = $__SpEL__0 and user.middleName = $__SpEL__1 RETURN user"); - - template = "MATCH (user:User) WHERE user.name=?#{[0]} and user.name=:#{[0]} RETURN user"; - spelExtractor = spelQueryContext.parse(template); - query = spelExtractor.getQueryString(); - - assertThat(query) - .isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user"); - } - - @Test - void shouldDetectInvalidAnnotation() { - - Neo4jQueryMethod method = neo4jQueryMethod("annotatedQueryWithoutTemplate"); - assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> StringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory)) - .withMessage("Expected @Query annotation to have a value, but it did not"); - } - - @Test // DATAGRAPH-1409 - void shouldDetectMissingCountQuery() { - - Neo4jQueryMethod method = neo4jQueryMethod("missingCountQuery", Pageable.class); - assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> StringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory)) - .withMessage("Expected paging query method to have a count query"); - } - - @Test // DATAGRAPH-1409 - void shouldAllowMissingCountOnSlicedQuery(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = neo4jQueryMethod("missingCountQueryOnSlice", Pageable.class); - StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - "(?s)You provided a string based query returning a slice for '.*\\.missingCountQueryOnSlice'\\. You might want to consider adding a count query if more slices than you expect are returned\\.")); - } - - @Test // DATAGRAPH-1440 - void shouldDetectMissingPlaceHoldersOnPagedQuery(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnPage", Pageable.class); - StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - "(?s)The custom query.*MATCH \\(n:Page\\) return n.*for '.*\\.missingPlaceHoldersOnPage' is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); - } - - @Test // DATAGRAPH-1440 - void shouldDetectMissingPlaceHoldersOnSlicedQuery(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnSlice", Pageable.class); - StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - "(?s)The custom query.*MATCH \\(n:Slice\\) return n.*is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); - } - - @Test // DATAGRAPH-1440 - void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = neo4jQueryMethod("findAllExtendedEntitiesWithCustomQuery", Sort.class); - AbstractNeo4jQuery query = - StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] {Sort.by("name").ascending() }); - - query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - ".*" + Pattern.quote("Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") + ".*")) - .anyMatch(s -> s.matches( - "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); - } - - @Test // DATAGRAPH-1440 - void shouldWarnWhenUsingSortedPageable(LogbackCapture logbackCapture) { - - Neo4jQueryMethod method = neo4jQueryMethod("noWarningsPerSe", Pageable.class); - AbstractNeo4jQuery query = - StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - ValueExpressionDelegate.create(), method, projectionFactory); - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); - - query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(logbackCapture.getFormattedMessages()) - .anyMatch(s -> s.matches( - ".*" + Pattern - .quote("Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") - + ".*")) - .anyMatch(s -> s.matches( - "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); - } - - @Test // DATAGRAPH-1454 - void orderBySpelShouldWork(LogbackCapture logbackCapture) { - - ConfigurableApplicationContext context = new GenericApplicationContext(); - context.getBeanFactory().registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), - new Neo4jEvaluationContextExtension()); - context.refresh(); - - ValueExpressionDelegate delegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); - - Neo4jQueryMethod method = neo4jQueryMethod("orderBySpel", Pageable.class); - StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - delegate, method, projectionFactory); - - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); - PreparedQuery pq = query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()) - .isEqualTo("MATCH (n:Test) RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); - assertThat(logbackCapture.getFormattedMessages()) - .noneMatch(s -> s.matches( - ".*Please specify the order in the query itself and use an unsorted page request\\..*")) - .noneMatch(s -> s.matches( - "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); - } - - @Test // DATAGRAPH-1454 - void literalReplacementsShouldWork() { - - ConfigurableApplicationContext context = new GenericApplicationContext(); - context.getBeanFactory().registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), - new Neo4jEvaluationContextExtension()); - context.refresh(); - - ValueExpressionDelegate delegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); - - Neo4jQueryMethod method = neo4jQueryMethod("makeStaticThingsDynamic", String.class, String.class, String.class, String.class, Sort.class); - StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - delegate, method, projectionFactory); - - Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { "A valid ", "dynamic Label", "dyn prop", "static value", - Sort.by("name").ascending() }); - PreparedQuery pq = query.prepareQuery( - TestEntity.class, - Collections.emptySet(), - parameterAccessor, - Neo4jQueryType.DEFAULT, - () -> (typeSystem, mapAccessor) -> new TestEntity(), - UnaryOperator.identity() - ); - assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()) - .isEqualTo("MATCH (n:`A valid dynamic Label`) SET n.`dyn prop` = 'static value' RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); - } - - @Test - void shouldBindParameters() { - - Neo4jQueryMethod method = RepositoryQueryTest.neo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, - String.class); - - StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); - - // skip conversion - doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); - - Map resolveParameters = repositoryQuery.bindParameters(new Neo4jParameterAccessor( - (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), new Object[] { "A String", "Another String" }), true, - UnaryOperator.identity()); - - assertThat(resolveParameters).containsEntry("0", "A String").containsEntry("1", "Another String"); - } - - @Test - void shouldResolveNamedParameters() { - - Neo4jQueryMethod method = RepositoryQueryTest.neo4jQueryMethod("findByDontDoThisInRealLiveNamed", - org.neo4j.driver.types.Point.class, String.class, String.class); - - StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); - - // skip conversion - doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); - - Point thePoint = Values.point(4223, 1, 2).asPoint(); - Map resolveParameters = repositoryQuery - .bindParameters(new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), - new Object[] { thePoint, "TheName", "TheFirstName" }), true, UnaryOperator.identity()); - - assertThat(resolveParameters).hasSize(8).containsEntry("0", thePoint).containsEntry("location", thePoint) - .containsEntry("1", "TheName").containsEntry("name", "TheName").containsEntry("2", "TheFirstName") - .containsEntry("firstName", "TheFirstName").containsEntry("__SpEL__0", "TheFirstName") - .containsEntry("__SpEL__1", "TheNameTheFirstName"); - } - } - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class ResultProcessTest { - - private Stream params() { - return Stream.of( - Arguments.of( - "findAllByANamedQuery", - false, - TestEntity.class, - TestEntity.class - ), - Arguments.of( - "findAllInterfaceProjectionsBy", - true, - TestEntityInterfaceProjection.class, - TestEntity.class - ), - Arguments.of( - "findAllDTOProjectionsBy", - true, - TestEntityDTOProjection.class, - TestEntity.class - ), - Arguments.of( - "findAllExtendedEntities", - false, - ExtendedTestEntity.class, - ExtendedTestEntity.class - ) - ); - } - - @ParameterizedTest - @MethodSource("params") - void shouldDetectCorrectProjectionBehaviour(String methodName, boolean projecting, Class queryReturnedType, Class domainType) { - - Neo4jQueryMethod method = RepositoryQueryTest.neo4jQueryMethod(methodName); - - ReturnedType returnedType = method.getResultProcessor().getReturnedType(); - assertThat(returnedType.isProjecting()).isEqualTo(projecting); - assertThat(returnedType.getReturnedType()).isEqualTo(queryReturnedType); - assertThat(returnedType.getDomainType()).isEqualTo(domainType); - assertThat(Neo4jQuerySupport.getDomainType(method)).isEqualTo(domainType); - } + private RepositoryQueryTests() { } private static Method queryMethod(String name, Class... parameters) { @@ -543,47 +127,34 @@ final class RepositoryQueryTest { private static ReactiveNeo4jQueryMethod reactiveNeo4jQueryMethod(String name, Class... parameters) { - return new ReactiveNeo4jQueryMethod(queryMethod(name, parameters), - TEST_REPOSITORY_METADATA, PROJECTION_FACTORY); + return new ReactiveNeo4jQueryMethod(queryMethod(name, parameters), TEST_REPOSITORY_METADATA, + PROJECTION_FACTORY); } - private static class TestEntity { - @Id @GeneratedValue private Long id; + @ParameterizedTest + @ValueSource(strings = { "RETURN 1 SKIP $skip LIMIT $limit", "RETURN 1 sKip $skip limit $limit", + "match(n) return $\n" + " skip \n" + " skip $\n" + + " skip \n" + " LIMIT $ limit", + "MATCH (n) RETURN n Skip $skip LIMIT /* No */ $ " + " /* NO */ limit", + "MATCH (n) RETURN n Skip $skip LIMIT /* No */$/* NO */ limit", + "MATCH (n) RETURN n Skip $skip LIMIT // No, no\n" + " $ /* really, not a */ limit" }) + void shouldDetectValidSkipAndLimitPlaceholders(String template) { - private String name; + assertThat(StringBasedNeo4jQuery.hasSkipAndLimitKeywordsAndPlaceholders(template)).isTrue(); } - private static class ExtendedTestEntity extends TestEntity { + @ParameterizedTest + @ValueSource(strings = { "RETURN 1 SKIP $SKIP LIMIT $LIMIT", "RETURN 1 skip $skiP limit $lImit", + "RETURN 1 skip // $skiP limit $lImit", "RETURN 1 skip $skiP limit // $lImit" }) + void shouldDetectValidSkipAndLimitPlaceholdersNegative(String template) { - private String otherAttribute; + assertThat(StringBasedNeo4jQuery.hasSkipAndLimitKeywordsAndPlaceholders(template)).isFalse(); } private interface TestEntityInterfaceProjection { String getName(); - } - private static class TestEntityDTOProjection { - - private String name; - - private Long numberOfRelations; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getNumberOfRelations() { - return numberOfRelations; - } - - public void setNumberOfRelations(Long numberOfRelations) { - this.numberOfRelations = numberOfRelations; - } } private interface TestRepository extends CrudRepository { @@ -627,29 +198,425 @@ final class RepositoryQueryTest { @Query(value = "MATCH (n:Test) RETURN n SKIP $skip LIMIT $limit", countQuery = "MATCH (n:Test) RETURN count(n)") Slice noWarningsPerSe(Pageable pageable); - // The complexity of the queries here doesn't matter, we the tests aim for having the appropriate skip/limits and count queries. + // The complexity of the queries here doesn't matter, we the tests aim for having + // the appropriate skip/limits and count queries. @Query(value = "MATCH (n:Page) return n", countQuery = "RETURN 1") Page missingPlaceHoldersOnPage(Pageable pageable); @Query(value = "MATCH (n:Slice) return n", countQuery = "RETURN 1") Slice missingPlaceHoldersOnSlice(Pageable pageable); - @Query(value = "MATCH (n:Test) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit", countQuery = "MATCH (n:Test) RETURN count(n)") + @Query(value = "MATCH (n:Test) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit", + countQuery = "MATCH (n:Test) RETURN count(n)") Slice orderBySpel(Pageable page); - @Query(value = "MATCH (n:`:#{literal(#aDynamicLabelPt1 + #aDynamicLabelPt2)}`) " - + "SET n.`:#{literal(#aDynamicProperty)}` = :#{literal('''' + #enforcedLiteralValue + '''')} " - + "RETURN n :#{orderBy(#sort)} SKIP $skip LIMIT $limit" - ) - List makeStaticThingsDynamic( - @Param("aDynamicLabelPt1") String aDynamicLabelPt1, - @Param("aDynamicLabelPt2") String aDynamicLabelPt2, - @Param("aDynamicProperty") String aDynamicProperty, - @Param("enforcedLiteralValue") String enforcedLiteralValue, - Sort sort - ); + @Query("MATCH (n:`:#{literal(#aDynamicLabelPt1 + #aDynamicLabelPt2)}`) " + + "SET n.`:#{literal(#aDynamicProperty)}` = :#{literal('''' + #enforcedLiteralValue + '''')} " + + "RETURN n :#{orderBy(#sort)} SKIP $skip LIMIT $limit") + List makeStaticThingsDynamic(@Param("aDynamicLabelPt1") String aDynamicLabelPt1, + @Param("aDynamicLabelPt2") String aDynamicLabelPt2, @Param("aDynamicProperty") String aDynamicProperty, + @Param("enforcedLiteralValue") String enforcedLiteralValue, Sort sort); + } - private RepositoryQueryTest() { + private static class TestEntity { + + @Id + @GeneratedValue + private Long id; + + private String name; + } + + private static final class ExtendedTestEntity extends TestEntity { + + private String otherAttribute; + + } + + private static final class TestEntityDTOProjection { + + private String name; + + private Long numberOfRelations; + + String getName() { + return this.name; + } + + void setName(String name) { + this.name = name; + } + + Long getNumberOfRelations() { + return this.numberOfRelations; + } + + void setNumberOfRelations(Long numberOfRelations) { + this.numberOfRelations = numberOfRelations; + } + + } + + @Nested + class Neo4jQueryMethodTest { + + @Test + void findQueryAnnotation() { + + Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("annotatedQueryWithValidTemplate"); + + Optional optionalQueryAnnotation = neo4jQueryMethod.getQueryAnnotation(); + assertThat(optionalQueryAnnotation).isPresent(); + } + + @Test + void streamQueriesShouldBeTreatedAsCollectionQueries() { + + Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("findAllByIdGreaterThan", long.class); + + assumeThat(neo4jQueryMethod.isStreamQuery()).isTrue(); + assertThat(neo4jQueryMethod.isCollectionLikeQuery()).isTrue(); + } + + @Test + void collectionQueriesShouldBeTreatedAsSuch() { + + Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("findAllByANamedQuery"); + + assumeThat(neo4jQueryMethod.isCollectionQuery()).isTrue(); + assertThat(neo4jQueryMethod.isCollectionLikeQuery()).isTrue(); + } + + @Test + void shouldFailOnMonoOfPageAsReturnType() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> reactiveNeo4jQueryMethod("findAllByName", String.class, Pageable.class)); + } + + @Test + void shouldFailForPageableParameterOnMonoOfPageAsReturnType() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> reactiveNeo4jQueryMethod("findAllByName", String.class, Pageable.class)); + } + + @Test + void shouldFailForPageableParameterOnMonoOfSliceAsReturnType() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> reactiveNeo4jQueryMethod("findAllByNameStartingWith", String.class, Pageable.class)); + } + + } + + @Nested + class Neo4jQueryLookupStrategyTest { + + @Test + void shouldSelectPartTreeNeo4jQuery() { + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy( + RepositoryQueryTests.this.neo4jOperations, RepositoryQueryTests.this.neo4jMappingContext, + ValueExpressionDelegate.create(), Configuration.defaultConfig()); + + RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("findById", Object.class), + TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, RepositoryQueryTests.this.namedQueries); + assertThat(query).isInstanceOf(PartTreeNeo4jQuery.class); + } + + @Test + void shouldSelectStringBasedNeo4jQuery() { + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy( + RepositoryQueryTests.this.neo4jOperations, RepositoryQueryTests.this.neo4jMappingContext, + ValueExpressionDelegate.create(), Configuration.defaultConfig()); + + RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("annotatedQueryWithValidTemplate"), + TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, RepositoryQueryTests.this.namedQueries); + assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); + } + + @Test + void shouldSelectStringBasedNeo4jQueryForNamedQuery() { + + final String namedQueryName = "TestEntity.findAllByANamedQuery"; + given(RepositoryQueryTests.this.namedQueries.hasQuery(namedQueryName)).willReturn(true); + given(RepositoryQueryTests.this.namedQueries.getQuery(namedQueryName)).willReturn("MATCH (n) RETURN n"); + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy( + RepositoryQueryTests.this.neo4jOperations, RepositoryQueryTests.this.neo4jMappingContext, + ValueExpressionDelegate.create(), Configuration.defaultConfig()); + + RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("findAllByANamedQuery"), + TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, RepositoryQueryTests.this.namedQueries); + assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); + } + + } + + @Nested + @ExtendWith(LogbackCapturingExtension.class) + class StringBasedNeo4jQueryTest { + + @Test + void spelQueryContextShouldBeConfiguredCorrectly() { + ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter spelQueryContext = ValueExpressionQueryRewriter + .of(ValueExpressionDelegate.create(), StringBasedNeo4jQuery::parameterNameSource, + StringBasedNeo4jQuery::replacementSource); + + String template; + String query; + ValueExpressionQueryRewriter.ParsedQuery spelExtractor; + + template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; + + spelExtractor = spelQueryContext.parse(template); + query = spelExtractor.getQueryString(); + + assertThat(query).isEqualTo( + "MATCH (user:User) WHERE user.name = $__SpEL__0 and user.middleName = $__SpEL__1 RETURN user"); + + template = "MATCH (user:User) WHERE user.name=?#{[0]} and user.name=:#{[0]} RETURN user"; + spelExtractor = spelQueryContext.parse(template); + query = spelExtractor.getQueryString(); + + assertThat(query) + .isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user"); + } + + @Test + void shouldDetectInvalidAnnotation() { + + Neo4jQueryMethod method = neo4jQueryMethod("annotatedQueryWithoutTemplate"); + assertThatExceptionOfType(MappingException.class) + .isThrownBy(() -> StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory)) + .withMessage("Expected @Query annotation to have a value, but it did not"); + } + + @Test // DATAGRAPH-1409 + void shouldDetectMissingCountQuery() { + + Neo4jQueryMethod method = neo4jQueryMethod("missingCountQuery", Pageable.class); + assertThatExceptionOfType(MappingException.class) + .isThrownBy(() -> StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory)) + .withMessage("Expected paging query method to have a count query"); + } + + @Test // DATAGRAPH-1409 + void shouldAllowMissingCountOnSlicedQuery(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = neo4jQueryMethod("missingCountQueryOnSlice", Pageable.class); + StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches( + "(?s)You provided a string based query returning a slice for '.*\\.missingCountQueryOnSlice'\\. You might want to consider adding a count query if more slices than you expect are returned\\.")); + } + + @Test // DATAGRAPH-1440 + void shouldDetectMissingPlaceHoldersOnPagedQuery(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnPage", Pageable.class); + StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches( + "(?s)The custom query.*MATCH \\(n:Page\\) return n.*for '.*\\.missingPlaceHoldersOnPage' is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); + } + + @Test // DATAGRAPH-1440 + void shouldDetectMissingPlaceHoldersOnSlicedQuery(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnSlice", Pageable.class); + StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches( + "(?s)The custom query.*MATCH \\(n:Slice\\) return n.*is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); + } + + @Test // DATAGRAPH-1440 + void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = neo4jQueryMethod("findAllExtendedEntitiesWithCustomQuery", Sort.class); + AbstractNeo4jQuery query = StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory); + + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { Sort.by("name").ascending() }); + + query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, Neo4jQueryType.DEFAULT, + () -> (typeSystem, mapAccessor) -> new TestEntity(), UnaryOperator.identity()); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches(".*" + Pattern.quote( + "Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") + + ".*")) + .anyMatch(s -> s.matches( + "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); + } + + @Test // DATAGRAPH-1440 + void shouldWarnWhenUsingSortedPageable(LogbackCapture logbackCapture) { + + Neo4jQueryMethod method = neo4jQueryMethod("noWarningsPerSe", Pageable.class); + AbstractNeo4jQuery query = StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, ValueExpressionDelegate.create(), method, + RepositoryQueryTests.this.projectionFactory); + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); + + query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, Neo4jQueryType.DEFAULT, + () -> (typeSystem, mapAccessor) -> new TestEntity(), UnaryOperator.identity()); + assertThat(logbackCapture.getFormattedMessages()).anyMatch(s -> s.matches(".*" + Pattern.quote( + "Please specify the order in the query itself and use an unsorted request or use the SpEL extension `:#{orderBy(#sort)}`.") + + ".*")) + .anyMatch(s -> s.matches( + "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); + } + + @Test // DATAGRAPH-1454 + void orderBySpelShouldWork(LogbackCapture logbackCapture) { + + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory() + .registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), + new Neo4jEvaluationContextExtension()); + context.refresh(); + + ValueExpressionDelegate delegate = new ValueExpressionDelegate( + new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); + + Neo4jQueryMethod method = neo4jQueryMethod("orderBySpel", Pageable.class); + StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, delegate, method, + RepositoryQueryTests.this.projectionFactory); + + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); + PreparedQuery pq = query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, + Neo4jQueryType.DEFAULT, () -> (typeSystem, mapAccessor) -> new TestEntity(), + UnaryOperator.identity()); + assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()) + .isEqualTo("MATCH (n:Test) RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); + assertThat(logbackCapture.getFormattedMessages()) + .noneMatch(s -> s + .matches(".*Please specify the order in the query itself and use an unsorted page request\\..*")) + .noneMatch(s -> s.matches( + "(?s).*One possible order clause matching your page request would be the following fragment:.*ORDER BY name ASC")); + } + + @Test // DATAGRAPH-1454 + void literalReplacementsShouldWork() { + + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory() + .registerSingleton(Neo4jEvaluationContextExtension.class.getSimpleName(), + new Neo4jEvaluationContextExtension()); + context.refresh(); + + ValueExpressionDelegate delegate = new ValueExpressionDelegate( + new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); + + Neo4jQueryMethod method = neo4jQueryMethod("makeStaticThingsDynamic", String.class, String.class, + String.class, String.class, Sort.class); + StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(RepositoryQueryTests.this.neo4jOperations, + RepositoryQueryTests.this.neo4jMappingContext, delegate, method, + RepositoryQueryTests.this.projectionFactory); + + Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( + (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), new Object[] { "A valid ", + "dynamic Label", "dyn prop", "static value", Sort.by("name").ascending() }); + PreparedQuery pq = query.prepareQuery(TestEntity.class, Collections.emptySet(), parameterAccessor, + Neo4jQueryType.DEFAULT, () -> (typeSystem, mapAccessor) -> new TestEntity(), + UnaryOperator.identity()); + assertThat(pq.getQueryFragmentsAndParameters().getCypherQuery()).isEqualTo( + "MATCH (n:`A valid dynamic Label`) SET n.`dyn prop` = 'static value' RETURN n ORDER BY name ASC SKIP $skip LIMIT $limit"); + } + + @Test + void shouldBindParameters() { + + Neo4jQueryMethod method = RepositoryQueryTests.neo4jQueryMethod("annotatedQueryWithValidTemplate", + String.class, String.class); + + StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create( + RepositoryQueryTests.this.neo4jOperations, RepositoryQueryTests.this.neo4jMappingContext, + ValueExpressionDelegate.create(), method, RepositoryQueryTests.this.projectionFactory)); + + // skip conversion + BDDMockito.doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); + + Map resolveParameters = repositoryQuery + .bindParameters(new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { "A String", "Another String" }), true, UnaryOperator.identity()); + + assertThat(resolveParameters).containsEntry("0", "A String").containsEntry("1", "Another String"); + } + + @Test + void shouldResolveNamedParameters() { + + Neo4jQueryMethod method = RepositoryQueryTests.neo4jQueryMethod("findByDontDoThisInRealLiveNamed", + org.neo4j.driver.types.Point.class, String.class, String.class); + + StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create( + RepositoryQueryTests.this.neo4jOperations, RepositoryQueryTests.this.neo4jMappingContext, + ValueExpressionDelegate.create(), method, RepositoryQueryTests.this.projectionFactory)); + + // skip conversion + BDDMockito.doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); + + Point thePoint = Values.point(4223, 1, 2).asPoint(); + Map resolveParameters = repositoryQuery + .bindParameters(new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) method.getParameters(), + new Object[] { thePoint, "TheName", "TheFirstName" }), true, UnaryOperator.identity()); + + assertThat(resolveParameters).hasSize(8) + .containsEntry("0", thePoint) + .containsEntry("location", thePoint) + .containsEntry("1", "TheName") + .containsEntry("name", "TheName") + .containsEntry("2", "TheFirstName") + .containsEntry("firstName", "TheFirstName") + .containsEntry("__SpEL__0", "TheFirstName") + .containsEntry("__SpEL__1", "TheNameTheFirstName"); + } + + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ResultProcessTest { + + private Stream params() { + return Stream.of(Arguments.of("findAllByANamedQuery", false, TestEntity.class, TestEntity.class), + Arguments.of("findAllInterfaceProjectionsBy", true, TestEntityInterfaceProjection.class, + TestEntity.class), + Arguments.of("findAllDTOProjectionsBy", true, TestEntityDTOProjection.class, TestEntity.class), + Arguments.of("findAllExtendedEntities", false, ExtendedTestEntity.class, ExtendedTestEntity.class)); + } + + @ParameterizedTest + @MethodSource("params") + void shouldDetectCorrectProjectionBehaviour(String methodName, boolean projecting, Class queryReturnedType, + Class domainType) { + + Neo4jQueryMethod method = RepositoryQueryTests.neo4jQueryMethod(methodName); + + ReturnedType returnedType = method.getResultProcessor().getReturnedType(); + assertThat(returnedType.isProjecting()).isEqualTo(projecting); + assertThat(returnedType.getReturnedType()).isEqualTo(queryReturnedType); + assertThat(returnedType.getDomainType()).isEqualTo(domainType); + assertThat(Neo4jQuerySupport.getDomainType(method)).isEqualTo(domainType); + } + + } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/TestEntity.java b/src/test/java/org/springframework/data/neo4j/repository/query/TestEntity.java index 6b8a7417d..8bb98f7df 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/TestEntity.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/TestEntity.java @@ -22,7 +22,11 @@ import org.springframework.data.neo4j.core.schema.GeneratedValue; * @author Michael J. Simons */ class TestEntity { - @Id @GeneratedValue private Long id; + + @Id + @GeneratedValue + private Long id; private String name; + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTest.java b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTests.java similarity index 74% rename from src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTest.java rename to src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTests.java index 080e2a037..1f2e9673a 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactorySupportTests.java @@ -15,16 +15,16 @@ */ package org.springframework.data.neo4j.repository.support; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.fail; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; + /** * @author Gerrit Meier */ -class Neo4jRepositoryFactorySupportTest { +class Neo4jRepositoryFactorySupportTests { @Nested class IdentifierTypeCheck { @@ -33,31 +33,33 @@ class Neo4jRepositoryFactorySupportTest { void mismatchingClassTypes() { assertThatIllegalArgumentException() - .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(String.class, Long.class)).withMessage( - "The repository id type class java.lang.String differs from the entity id type class java.lang.Long"); + .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(String.class, Long.class)) + .withMessage( + "The repository id type class java.lang.String differs from the entity id type class java.lang.Long"); } @Test void mismatchingPrimitiveTypes() { assertThatIllegalArgumentException() - .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(int.class, long.class)) - .withMessage("The repository id type int differs from the entity id type long"); + .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(int.class, long.class)) + .withMessage("The repository id type int differs from the entity id type long"); } @Test void mismatchingPrimitiveAndClassTypes() { assertThatIllegalArgumentException() - .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(Integer.class, long.class)) - .withMessage("The repository id type class java.lang.Integer differs from the entity id type long"); + .isThrownBy(() -> Neo4jRepositoryFactorySupport.assertIdentifierType(Integer.class, long.class)) + .withMessage("The repository id type class java.lang.Integer differs from the entity id type long"); } @Test void matchingPrimitiveLongTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(long.class, long.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } } @@ -66,7 +68,8 @@ class Neo4jRepositoryFactorySupportTest { void matchingPrimitiveIntTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(int.class, int.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } } @@ -75,7 +78,8 @@ class Neo4jRepositoryFactorySupportTest { void matchingPrimitiveIntAndIntegerClassTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(int.class, Integer.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } } @@ -84,7 +88,8 @@ class Neo4jRepositoryFactorySupportTest { void matchingIntegerClassAndPrimitiveIntTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(Integer.class, int.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } @@ -94,7 +99,8 @@ class Neo4jRepositoryFactorySupportTest { void matchingPrimitiveLongAndLongClassTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(long.class, Long.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } } @@ -103,10 +109,12 @@ class Neo4jRepositoryFactorySupportTest { void matchingLongClassAndPrimitiveLongTypes() { try { Neo4jRepositoryFactorySupport.assertIdentifierType(Long.class, long.class); - } catch (Exception e) { + } + catch (Exception ex) { fail("no exception should get thrown."); } } } + } diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTests.java similarity index 51% rename from src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java rename to src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTests.java index 68b4b5591..718804272 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTests.java @@ -15,12 +15,6 @@ */ package org.springframework.data.neo4j.repository.support; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.util.Arrays; import java.util.HashSet; import java.util.Optional; @@ -34,64 +28,113 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.geo.Point; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes; import org.springframework.data.neo4j.integration.shared.common.ThingWithAllCypherTypes; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes; import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCompositeProperties; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryCreationException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + /** * @author Gerrit Meier * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class Neo4jRepositoryFactoryTest { +class Neo4jRepositoryFactoryTests { + + interface InvalidIgnoreCase extends Neo4jRepository { + + Optional findOneByAnIntIgnoreCase(int anInt); + + } + + interface InvalidTemporal extends Neo4jRepository { + + Optional findOneByAnIntAfter(int anInt); + + } + + interface InvalidCollection extends Neo4jRepository { + + Optional findOneByALongIsEmpty(); + + } + + interface InvalidSpatial extends Neo4jRepository { + + Optional findOneByALongIsNear(Point point); + + } + + interface InvalidDeleteBy extends Neo4jRepository { + + Optional deleteAllBy(Point point); + + } + + interface DerivedWithComposite extends Neo4jRepository { + + Optional findOneByCustomTypeMapTrue(); + + } /** - * Test failure and success to ensure that {@link Neo4jRepositoryFactorySupport#assertIdentifierType(Class, Class)} - * gets used. + * Test failure and success to ensure that + * {@link Neo4jRepositoryFactorySupport#assertIdentifierType(Class, Class)} gets used. */ @Nested class IdentifierTypeCheck { - @Spy private Neo4jRepositoryFactory neo4jRepositoryFactory = new Neo4jRepositoryFactory(null, null); + + @Spy + private Neo4jRepositoryFactory neo4jRepositoryFactory = new Neo4jRepositoryFactory(null, null); + private Neo4jEntityInformation entityInformation; + private RepositoryInformation metadata; @BeforeEach void setup() { - metadata = mock(RepositoryInformation.class); - entityInformation = mock(Neo4jEntityInformation.class); + this.metadata = mock(RepositoryInformation.class); + this.entityInformation = mock(Neo4jEntityInformation.class); - doReturn(entityInformation).when(neo4jRepositoryFactory).getEntityInformation(Mockito.any(RepositoryMetadata.class)); + doReturn(this.entityInformation).when(this.neo4jRepositoryFactory) + .getEntityInformation(Mockito.any(RepositoryMetadata.class)); } @Test void matchingClassTypes() { - when(entityInformation.getIdType()).thenReturn(Long.class); + given(this.entityInformation.getIdType()).willReturn(Long.class); Class repositoryIdentifierClass = Long.class; - when(metadata.getIdType()).thenReturn(repositoryIdentifierClass); + given(this.metadata.getIdType()).willReturn(repositoryIdentifierClass); - assertThatThrownBy(() -> neo4jRepositoryFactory.getTargetRepository(metadata)) - .hasMessageContaining("Target type must not be null").isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> this.neo4jRepositoryFactory.getTargetRepository(this.metadata)) + .hasMessageContaining("Target type must not be null") + .isInstanceOf(IllegalArgumentException.class); } @Test void mismatchingClassTypes() { - when(entityInformation.getIdType()).thenReturn(Long.class); + given(this.entityInformation.getIdType()).willReturn(Long.class); Class repositoryIdentifierClass = String.class; - when(metadata.getIdType()).thenReturn(repositoryIdentifierClass); + given(this.metadata.getIdType()).willReturn(repositoryIdentifierClass); - assertThatThrownBy(() -> neo4jRepositoryFactory.getTargetRepository(metadata)) - .hasMessage( - "The repository id type class java.lang.String differs from the entity id type class java.lang.Long") - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> this.neo4jRepositoryFactory.getTargetRepository(this.metadata)).hasMessage( + "The repository id type class java.lang.String differs from the entity id type class java.lang.Long") + .isInstanceOf(IllegalArgumentException.class); } + } @Nested @@ -99,89 +142,72 @@ class Neo4jRepositoryFactoryTest { class DerivedQueryCheck { private Neo4jMappingContext mappingContext; + private Neo4jRepositoryFactory repositoryFactory; @BeforeAll void prepareContext() { - mappingContext = new Neo4jMappingContext(); - mappingContext.setInitialEntitySet(new HashSet<>( - Arrays.asList(ThingWithAllAdditionalTypes.class, - ThingWithAllCypherTypes.class, - ThingWithCompositeProperties.class))); - repositoryFactory = new Neo4jRepositoryFactory(Mockito.mock(Neo4jTemplate.class), mappingContext); + this.mappingContext = new Neo4jMappingContext(); + this.mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(ThingWithAllAdditionalTypes.class, + ThingWithAllCypherTypes.class, ThingWithCompositeProperties.class))); + this.repositoryFactory = new Neo4jRepositoryFactory(Mockito.mock(Neo4jTemplate.class), this.mappingContext); } @Test void validateIgnoreCaseShouldWork() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidIgnoreCase.class)) - .withMessageMatching("Could not create query for .*: Only the case of String based properties can be ignored within the following keywords: \\[IsNotLike, NotLike, IsLike, Like, IsStartingWith, StartingWith, StartsWith, IsEndingWith, EndingWith, EndsWith, IsNotContaining, NotContaining, NotContains, IsContaining, Containing, Contains, IsNot, Not, Is, Equals]"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(InvalidIgnoreCase.class)) + .withMessageMatching( + "Could not create query for .*: Only the case of String based properties can be ignored within the following keywords: \\[IsNotLike, NotLike, IsLike, Like, IsStartingWith, StartingWith, StartsWith, IsEndingWith, EndingWith, EndsWith, IsNotContaining, NotContaining, NotContains, IsContaining, Containing, Contains, IsNot, Not, Is, Equals]"); } @Test void validateTemporalShouldWork() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidTemporal.class)) - .withMessageMatching("Could not create query for .*: The keywords \\[IsAfter, After] work only with properties with one of the following types: \\[class java.time.Instant, class java.time.LocalDate, class java.time.LocalDateTime, class java.time.LocalTime, class java.time.OffsetDateTime, class java.time.OffsetTime, class java.time.ZonedDateTime]"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(InvalidTemporal.class)) + .withMessageMatching( + "Could not create query for .*: The keywords \\[IsAfter, After] work only with properties with one of the following types: \\[class java.time.Instant, class java.time.LocalDate, class java.time.LocalDateTime, class java.time.LocalTime, class java.time.OffsetDateTime, class java.time.OffsetTime, class java.time.ZonedDateTime]"); } @Test void validateCollectionShouldWork() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidCollection.class)) - .withMessageMatching("Could not create query for .*: The keywords \\[IsEmpty, Empty] work only with collection properties"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(InvalidCollection.class)) + .withMessageMatching( + "Could not create query for .*: The keywords \\[IsEmpty, Empty] work only with collection properties"); } @Test void validateSpatialShouldWork() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidSpatial.class)) - .withMessageMatching("Could not create query for .* \\[IsNear, Near] works only with spatial properties"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(InvalidSpatial.class)) + .withMessageMatching( + "Could not create query for .* \\[IsNear, Near] works only with spatial properties"); } @Test void validateNotACompositePropertyShouldWork() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(DerivedWithComposite.class)) - .withMessageMatching("Could not create query for .*: Derived queries are not supported for composite properties"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(DerivedWithComposite.class)) + .withMessageMatching( + "Could not create query for .*: Derived queries are not supported for composite properties"); } @Test // GH-2281 void validateDeleteReturnType() { - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidDeleteBy.class)) - .withMessageMatching("Could not create query for .*: A derived delete query can only return the number of deleted nodes as a long or void"); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> this.repositoryFactory.getRepository(InvalidDeleteBy.class)) + .withMessageMatching( + "Could not create query for .*: A derived delete query can only return the number of deleted nodes as a long or void"); } + } - interface InvalidIgnoreCase extends Neo4jRepository { - - Optional findOneByAnIntIgnoreCase(int anInt); - } - - interface InvalidTemporal extends Neo4jRepository { - - Optional findOneByAnIntAfter(int anInt); - } - - interface InvalidCollection extends Neo4jRepository { - - Optional findOneByALongIsEmpty(); - } - - interface InvalidSpatial extends Neo4jRepository { - - Optional findOneByALongIsNear(Point point); - } - - interface InvalidDeleteBy extends Neo4jRepository { - - Optional deleteAllBy(Point point); - } - - interface DerivedWithComposite extends Neo4jRepository { - - Optional findOneByCustomTypeMapTrue(); - } } diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTests.java similarity index 56% rename from src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java rename to src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTests.java index a0dc56fa5..c559d5b18 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTests.java @@ -15,11 +15,6 @@ */ package org.springframework.data.neo4j.repository.support; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -27,57 +22,68 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + /** * @author Gerrit Meier * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) -class ReactiveNeo4jRepositoryFactoryTest { +class ReactiveNeo4jRepositoryFactoryTests { /** - * Test failure and success to ensure that {@link Neo4jRepositoryFactorySupport#assertIdentifierType(Class, Class)} - * gets used. + * Test failure and success to ensure that + * {@link Neo4jRepositoryFactorySupport#assertIdentifierType(Class, Class)} gets used. */ @Nested class IdentifierTypeCheck { - @Spy private ReactiveNeo4jRepositoryFactory neo4jRepositoryFactory = new ReactiveNeo4jRepositoryFactory(null, null); + @Spy + private ReactiveNeo4jRepositoryFactory neo4jRepositoryFactory = new ReactiveNeo4jRepositoryFactory(null, null); + private Neo4jEntityInformation entityInformation; + private RepositoryInformation metadata; @BeforeEach void setup() { - metadata = mock(RepositoryInformation.class); - entityInformation = mock(Neo4jEntityInformation.class); + this.metadata = mock(RepositoryInformation.class); + this.entityInformation = mock(Neo4jEntityInformation.class); - doReturn(entityInformation).when(neo4jRepositoryFactory).getEntityInformation(Mockito.any(RepositoryMetadata.class)); + doReturn(this.entityInformation).when(this.neo4jRepositoryFactory) + .getEntityInformation(Mockito.any(RepositoryMetadata.class)); } @Test void matchingClassTypes() { - when(entityInformation.getIdType()).thenReturn(Long.class); + given(this.entityInformation.getIdType()).willReturn(Long.class); Class repositoryIdentifierClass = Long.class; - when(metadata.getIdType()).thenReturn(repositoryIdentifierClass); + given(this.metadata.getIdType()).willReturn(repositoryIdentifierClass); - assertThatThrownBy(() -> neo4jRepositoryFactory.getTargetRepository(metadata)) - .hasMessageContaining("Target type must not be null").isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> this.neo4jRepositoryFactory.getTargetRepository(this.metadata)) + .hasMessageContaining("Target type must not be null") + .isInstanceOf(IllegalArgumentException.class); } @Test void mismatchingClassTypes() { - when(entityInformation.getIdType()).thenReturn(Long.class); + given(this.entityInformation.getIdType()).willReturn(Long.class); Class repositoryIdentifierClass = String.class; - when(metadata.getIdType()).thenReturn(repositoryIdentifierClass); + given(this.metadata.getIdType()).willReturn(repositoryIdentifierClass); - assertThatThrownBy(() -> neo4jRepositoryFactory.getTargetRepository(metadata)) - .hasMessage( - "The repository id type class java.lang.String differs from the entity id type class java.lang.Long") - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> this.neo4jRepositoryFactory.getTargetRepository(this.metadata)).hasMessage( + "The repository id type class java.lang.String differs from the entity id type class java.lang.Long") + .isInstanceOf(IllegalArgumentException.class); } + } } diff --git a/src/test/java/org/springframework/data/neo4j/support/ProxyImageNameSubstitutor.java b/src/test/java/org/springframework/data/neo4j/support/ProxyImageNameSubstitutor.java index 4606c64c1..136b1a9a4 100644 --- a/src/test/java/org/springframework/data/neo4j/support/ProxyImageNameSubstitutor.java +++ b/src/test/java/org/springframework/data/neo4j/support/ProxyImageNameSubstitutor.java @@ -15,16 +15,15 @@ */ package org.springframework.data.neo4j.support; -import ch.qos.logback.classic.Logger; - import java.util.List; +import ch.qos.logback.classic.Logger; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.ImageNameSubstitutor; /** - * An {@link ImageNameSubstitutor} only used on CI servers to leverage internal proxy solution, that needs to vary the - * prefix based on which container image is needed. + * An {@link ImageNameSubstitutor} only used on CI servers to leverage internal proxy + * solution, that needs to vary the prefix based on which container image is needed. * * @author Greg Turnquist */ @@ -40,6 +39,20 @@ public class ProxyImageNameSubstitutor extends ImageNameSubstitutor { private static final String LIBRARY_PROXY_PREFIX = PROXY_PREFIX + "library/"; + /** + * Apply a non-library-based prefix. + */ + private static String applyProxyPrefix(String imageName) { + return PROXY_PREFIX + imageName; + } + + /** + * Apply a library based prefix. + */ + private static String applyProxyAndLibraryPrefix(String imageName) { + return LIBRARY_PROXY_PREFIX + imageName; + } + @Override public DockerImageName apply(DockerImageName dockerImageName) { @@ -66,17 +79,4 @@ public class ProxyImageNameSubstitutor extends ImageNameSubstitutor { return "Spring Data Proxy Image Name Substitutor"; } - /** - * Apply a non-library-based prefix. - */ - private static String applyProxyPrefix(String imageName) { - return PROXY_PREFIX + imageName; - } - - /** - * Apply a library based prefix. - */ - private static String applyProxyAndLibraryPrefix(String imageName) { - return LIBRARY_PROXY_PREFIX + imageName; - } } diff --git a/src/test/java/org/springframework/data/neo4j/test/BookmarkCapture.java b/src/test/java/org/springframework/data/neo4j/test/BookmarkCapture.java index a1943cf15..bab632bb9 100644 --- a/src/test/java/org/springframework/data/neo4j/test/BookmarkCapture.java +++ b/src/test/java/org/springframework/data/neo4j/test/BookmarkCapture.java @@ -26,36 +26,40 @@ import java.util.function.Supplier; import org.neo4j.driver.Bookmark; import org.neo4j.driver.SessionConfig; + import org.springframework.context.ApplicationListener; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarksUpdatedEvent; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.util.StringUtils; /** - * This is a utility class that captures the most recent bookmarks after any of the Spring Data Neo4j transaction managers - * commits a transaction. It also can preload the bookmarks; + * This is a utility class that captures the most recent bookmarks after any of the Spring + * Data Neo4j transaction managers commits a transaction. It also can preload the + * bookmarks; * * @author Michael J. Simons - * @soundtrack Black Sabbath - Master Of Reality */ public final class BookmarkCapture implements Supplier>, ApplicationListener { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Lock read = lock.readLock(); - private final Lock write = lock.writeLock(); - private Set latestBookmarks; + private final Lock read = this.lock.readLock(); + + private final Lock write = this.lock.writeLock(); private final Set nextBookmarks = new HashSet<>(); + private Set latestBookmarks; + public SessionConfig createSessionConfig() { return createSessionConfig(null, null); } public SessionConfig createSessionConfig(String databaseName, String impersonatedUser) { try { - read.lock(); - SessionConfig.Builder builder = SessionConfig.builder().withBookmarks(latestBookmarks == null ? Collections.emptyList() : latestBookmarks); + this.read.lock(); + SessionConfig.Builder builder = SessionConfig.builder() + .withBookmarks((this.latestBookmarks != null) ? this.latestBookmarks : Collections.emptyList()); if (StringUtils.hasText(databaseName)) { builder.withDatabase(databaseName); } @@ -63,8 +67,9 @@ public final class BookmarkCapture implements Supplier>, Applicati Neo4jTransactionUtils.withImpersonatedUser(builder, impersonatedUser); } return builder.build(); - } finally { - read.unlock(); + } + finally { + this.read.unlock(); } } @@ -76,31 +81,35 @@ public final class BookmarkCapture implements Supplier>, Applicati public void seedWith(Collection bookmarks) { try { - write.lock(); - nextBookmarks.addAll(bookmarks); - } finally { - write.unlock(); + this.write.lock(); + this.nextBookmarks.addAll(bookmarks); + } + finally { + this.write.unlock(); } } @Override public void onApplicationEvent(Neo4jBookmarksUpdatedEvent event) { try { - write.lock(); - latestBookmarks = event.getBookmarks(); - nextBookmarks.clear(); - } finally { - write.unlock(); + this.write.lock(); + this.latestBookmarks = event.getBookmarks(); + this.nextBookmarks.clear(); + } + finally { + this.write.unlock(); } } @Override public Set get() { try { - read.lock(); - return nextBookmarks; - } finally { - read.unlock(); + this.read.lock(); + return this.nextBookmarks; + } + finally { + this.read.unlock(); } } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/CausalClusterIntegrationTest.java b/src/test/java/org/springframework/data/neo4j/test/CausalClusterIntegrationTest.java index bc9e9b73e..bbe2ad1bb 100644 --- a/src/test/java/org/springframework/data/neo4j/test/CausalClusterIntegrationTest.java +++ b/src/test/java/org/springframework/data/neo4j/test/CausalClusterIntegrationTest.java @@ -24,16 +24,17 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.junit.jupiter.causal_cluster.NeedsCausalCluster; + import org.springframework.test.context.junit.jupiter.SpringExtension; /** - * Base annotation for tests that depend on a Causal Cluster. The causal cluster setup via Docker puts a high load on - * the system and also requires acceptance of the commercial license. Therefore it is only enabled when the environment - * variable {@literal SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION} is set to {@literal yes} and a {@literal SDN_NEO4J_VERSION} - * points to a stable 4.0.x version. + * Base annotation for tests that depend on a Causal Cluster. The causal cluster setup via + * Docker puts a high load on the system and also requires acceptance of the commercial + * license. Therefore it is only enabled when the environment variable + * {@literal SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION} is set to {@literal yes} and a + * {@literal SDN_NEO4J_VERSION} points to a stable 4.0.x version. * * @author Michael J. Simons - * @soundtrack Command & Conquer - Alarmstufe Rot * @since 6.0 */ @Target(ElementType.TYPE) @@ -44,4 +45,5 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; @EnabledIfEnvironmentVariable(named = "SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION", matches = "yes") @EnabledIfEnvironmentVariable(named = "SDN_NEO4J_VERSION", matches = "4\\.0\\.\\d(-.+)?") public @interface CausalClusterIntegrationTest { + } diff --git a/src/test/java/org/springframework/data/neo4j/test/DriverMocks.java b/src/test/java/org/springframework/data/neo4j/test/DriverMocks.java index e0baf11ca..57f3239f2 100644 --- a/src/test/java/org/springframework/data/neo4j/test/DriverMocks.java +++ b/src/test/java/org/springframework/data/neo4j/test/DriverMocks.java @@ -15,13 +15,6 @@ */ package org.springframework.data.neo4j.test; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import reactor.core.publisher.Mono; - import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; @@ -29,31 +22,41 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.reactivestreams.ReactiveSession; import org.neo4j.driver.reactivestreams.ReactiveTransaction; +import reactor.core.publisher.Mono; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** - * Some preconfigured driver mocks, mainly used to for Spring Integration tests where the behaviour of configuration and - * integration with Spring is tested and not with the database. + * Some preconfigured driver mocks, mainly used to for Spring Integration tests where the + * behaviour of configuration and integration with Spring is tested and not with the + * database. * * @author Michael J. Simons - * @soundtrack Elton John - Greatest Hits 1970-2002 * @since 6.0 */ public final class DriverMocks { + private DriverMocks() { + } + /** - * @return An instance usable in a test where an open session with an ongoing transaction is required. + * @return An instance usable in a test where an open session with an ongoing + * transaction is required. */ public static Driver withOpenSessionAndTransaction() { Transaction transaction = mock(Transaction.class); - when(transaction.isOpen()).thenReturn(true); + given(transaction.isOpen()).willReturn(true); Session session = mock(Session.class); - when(session.isOpen()).thenReturn(true); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + given(session.isOpen()).willReturn(true); + given(session.beginTransaction(any(TransactionConfig.class))).willReturn(transaction); Driver driver = mock(Driver.class); - when(driver.session(any(SessionConfig.class))).thenReturn(session); + given(driver.session(any(SessionConfig.class))).willReturn(session); return driver; } @@ -62,12 +65,11 @@ public final class DriverMocks { ReactiveTransaction transaction = mock(ReactiveTransaction.class); ReactiveSession session = mock(ReactiveSession.class); - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(transaction)); + given(session.beginTransaction(any(TransactionConfig.class))).willReturn(Mono.just(transaction)); Driver driver = mock(Driver.class); - when(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).thenReturn(session); + given(driver.session(eq(ReactiveSession.class), any(SessionConfig.class))).willReturn(session); return driver; } - private DriverMocks() {} } diff --git a/src/test/java/org/springframework/data/neo4j/test/LogbackCapture.java b/src/test/java/org/springframework/data/neo4j/test/LogbackCapture.java index daccd6fab..f54bc32ba 100644 --- a/src/test/java/org/springframework/data/neo4j/test/LogbackCapture.java +++ b/src/test/java/org/springframework/data/neo4j/test/LogbackCapture.java @@ -15,32 +15,33 @@ */ package org.springframework.data.neo4j.test; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; - import java.util.HashMap; import java.util.List; import java.util.Map; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import org.junit.jupiter.api.extension.ExtensionContext; /** * Provides access to the formatted message captured from Logback during test run. * * @author Michael J. Simons - * @soundtrack Various - Just The Best 90s */ public final class LogbackCapture implements ExtensionContext.Store.CloseableResource { private final ListAppender listAppender; + private final Logger logger; + private final Map additionalLoggers = new HashMap<>(); LogbackCapture() { this.listAppender = new ListAppender<>(); - // While forbidden by our checkstyle, we must go that route to get the logback root logger. + // While forbidden by our checkstyle, we must go that route to get the logback + // root logger. this.logger = (Logger) org.slf4j.LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); } @@ -53,11 +54,11 @@ public final class LogbackCapture implements ExtensionContext.Store.CloseableRes } public List getFormattedMessages() { - return listAppender.list.stream().map(ILoggingEvent::getFormattedMessage).toList(); + return this.listAppender.list.stream().map(ILoggingEvent::getFormattedMessage).toList(); } void start() { - this.logger.addAppender(listAppender); + this.logger.addAppender(this.listAppender); this.listAppender.start(); } @@ -70,10 +71,11 @@ public final class LogbackCapture implements ExtensionContext.Store.CloseableRes public void close() { this.resetLogLevel(); this.listAppender.stop(); - this.logger.detachAppender(listAppender); + this.logger.detachAppender(this.listAppender); } public void resetLogLevel() { this.additionalLoggers.forEach(Logger::setLevel); } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/LogbackCapturingExtension.java b/src/test/java/org/springframework/data/neo4j/test/LogbackCapturingExtension.java index 20556d8d5..6d5efbf03 100644 --- a/src/test/java/org/springframework/data/neo4j/test/LogbackCapturingExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/LogbackCapturingExtension.java @@ -23,10 +23,10 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; /** - * Naive extension to capture logging output. It assumes that logback is used during tests as logger binding. + * Naive extension to capture logging output. It assumes that logback is used during tests + * as logger binding. * * @author Michael J. Simons - * @soundtrack Various - Just The Best 90s */ public final class LogbackCapturingExtension implements BeforeAllCallback, AfterEachCallback, ParameterResolver { @@ -57,4 +57,5 @@ public final class LogbackCapturingExtension implements BeforeAllCallback, After public void afterEach(ExtensionContext context) { getOutputCapture(context).clear(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index 8db270a01..b46712e67 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -15,6 +15,15 @@ */ package org.springframework.data.neo4j.test; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.DefaultThreadFactory; @@ -38,61 +47,69 @@ import org.neo4j.driver.internal.DriverFactory; import org.neo4j.driver.internal.SecuritySettings; import org.neo4j.driver.internal.security.SecurityPlan; import org.neo4j.driver.internal.security.SecurityPlans; -import org.springframework.core.log.LogMessage; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.utility.TestcontainersConfiguration; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.net.URI; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import org.springframework.core.log.LogMessage; import static org.assertj.core.api.Assumptions.assumeThat; /** - * This extension is for internal use only. It is meant to speed up development and keep test containers for normal - * build. When both {@code SDN_NEO4J_URL} and {@code SDN_NEO4J_PASSWORD} are set as environment variables, the extension - * will inject a field of type {@link Neo4jConnectionSupport} into the extended test with a connection to that instance, - * otherwise it will start a test container and use that connection. + * This extension is for internal use only. It is meant to speed up development and keep + * test containers for normal build. When both {@code SDN_NEO4J_URL} and + * {@code SDN_NEO4J_PASSWORD} are set as environment variables, the extension will inject + * a field of type {@link Neo4jConnectionSupport} into the extended test with a connection + * to that instance, otherwise it will start a test container and use that connection. * * @author Michael J. Simons * @since 6.0 */ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { - public final static String NEEDS_REACTIVE_SUPPORT = "reactive-test"; - public final static String NEEDS_VERSION_SUPPORTING_ELEMENT_ID = "elementid-test"; - public final static String COMMUNITY_EDITION_ONLY = "community-edition"; - public final static String COMMERCIAL_EDITION_ONLY = "commercial-edition"; + public static final String NEEDS_REACTIVE_SUPPORT = "reactive-test"; + + public static final String NEEDS_VERSION_SUPPORTING_ELEMENT_ID = "elementid-test"; + + public static final String COMMUNITY_EDITION_ONLY = "community-edition"; + + public static final String COMMERCIAL_EDITION_ONLY = "commercial-edition"; + /** - * Indicator that a given _test_ is not compatible in all cases with a cluster setup, especially in terms of - * synchronizing bookmarks between fixture / assertions and tests. Or it may indicate a dedicated cluster test, running - * against a dedicated extension. + * Indicator that a given _test_ is not compatible in all cases with a cluster setup, + * especially in terms of synchronizing bookmarks between fixture / assertions and + * tests. Or it may indicate a dedicated cluster test, running against a dedicated + * extension. */ - public final static String INCOMPATIBLE_WITH_CLUSTERS = "incompatible-with-clusters"; - public final static String REQUIRES = "Neo4j/"; + public static final String INCOMPATIBLE_WITH_CLUSTERS = "incompatible-with-clusters"; + + public static final String REQUIRES = "Neo4j/"; private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(Neo4jExtension.class); private static final String KEY_NEO4J_INSTANCE = "neo4j.standalone"; + private static final String KEY_DRIVER_INSTANCE = "neo4j.driver"; private static final String SYS_PROPERTY_NEO4J_URL = "SDN_NEO4J_URL"; + private static final String SYS_PROPERTY_NEO4J_PASSWORD = "SDN_NEO4J_PASSWORD"; + private static final String SYS_PROPERTY_NEO4J_ACCEPT_COMMERCIAL_EDITION = "SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION"; + private static final String SYS_PROPERTY_NEO4J_REPOSITORY = "SDN_NEO4J_REPOSITORY"; + private static final String SYS_PROPERTY_NEO4J_VERSION = "SDN_NEO4J_VERSION"; + private static final String SYS_PROPERTY_FORCE_CONTAINER_REUSE = "SDN_FORCE_REUSE_OF_CONTAINERS"; + private static final Log log = org.apache.commons.logging.LogFactory.getLog(Neo4jExtension.class); private static final Set COMMUNITY_EDITION_INDICATOR = Set.of("community"); + private static final Set COMMERCIAL_EDITION_INDICATOR = Set.of("commercial", "enterprise"); - private static final EventLoopGroup EVENT_LOOP_GROUP = new NioEventLoopGroup(new DefaultThreadFactory(Neo4jExtension.class, true)); + private static final EventLoopGroup EVENT_LOOP_GROUP = new NioEventLoopGroup( + new DefaultThreadFactory(Neo4jExtension.class, true)); @Override public void beforeAll(ExtensionContext context) throws Exception { @@ -109,13 +126,15 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { String neo4jPassword = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_PASSWORD)).orElse("").trim(); ExtensionContext.Store contextStore = context.getStore(NAMESPACE); - Neo4jConnectionSupport neo4jConnectionSupport = contextStore.get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore.get(KEY_DRIVER_INSTANCE, + Neo4jConnectionSupport.class); if (neo4jConnectionSupport == null) { if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { log.info(LogMessage.format("Using Neo4j instance at %s.", neo4jUrl)); neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, AuthTokens.basic("neo4j", neo4jPassword)); - } else { + } + else { log.info("Using Neo4j test container."); ContainerAdapter adapter = contextStore.getOrComputeIfAbsent(KEY_NEO4J_INSTANCE, key -> new Neo4jExtension.ContainerAdapter(), ContainerAdapter.class); @@ -135,51 +154,58 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { @Override public void beforeEach(ExtensionContext context) { ExtensionContext.Store contextStore = context.getStore(NAMESPACE); - Neo4jConnectionSupport neo4jConnectionSupport = contextStore.get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore.get(KEY_DRIVER_INSTANCE, + Neo4jConnectionSupport.class); checkRequiredFeatures(neo4jConnectionSupport, context.getTags()); } private void checkRequiredFeatures(Neo4jConnectionSupport neo4jConnectionSupport, Set tags) { if (tags.contains(NEEDS_REACTIVE_SUPPORT)) { assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v4_0_0)) - .describedAs("This test requires at least Neo4j 4.0 for reactive database connectivity.").isTrue(); + .describedAs("This test requires at least Neo4j 4.0 for reactive database connectivity.") + .isTrue(); } if (tags.contains(NEEDS_VERSION_SUPPORTING_ELEMENT_ID)) { assumeThat(neo4jConnectionSupport.getServerVersion().greaterThan(ServerVersion.v5_3_0)) - .describedAs("This test requires a version greater than Neo4j 5.3.0 for correct elementId handling.").isTrue(); + .describedAs("This test requires a version greater than Neo4j 5.3.0 for correct elementId handling.") + .isTrue(); } if (tags.contains(COMMUNITY_EDITION_ONLY)) { assumeThat(neo4jConnectionSupport.isCommunityEdition()) - .describedAs("This test should be run on the community edition only").isTrue(); + .describedAs("This test should be run on the community edition only") + .isTrue(); } if (tags.contains(COMMERCIAL_EDITION_ONLY)) { assumeThat(neo4jConnectionSupport.isCommercialEdition()) - .describedAs("This test should be run on the commercial edition only").isTrue(); + .describedAs("This test should be run on the commercial edition only") + .isTrue(); } tags.stream().filter(s -> s.startsWith(REQUIRES)).map(ServerVersion::version).forEach(v -> { assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(v)) - .describedAs("This test requires at least " + v.toString()).isTrue(); + .describedAs("This test requires at least " + v.toString()) + .isTrue(); }); } /** - * Support class that holds the connection information and opens a new connection on demand. + * Support class that holds the connection information and opens a new connection on + * demand. * * @since 6.0 */ public static final class Neo4jConnectionSupport implements ExtensionContext.Store.CloseableResource { - private final DriverFactory driverFactory; - public final URI uri; public final AuthToken authToken; public final Config config; + private final DriverFactory driverFactory; + private final SecurityPlan securityPlan; private volatile ServerVersion cachedServerVersion; @@ -192,20 +218,46 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { public Neo4jConnectionSupport(String url, AuthToken authToken) { this.uri = URI.create(url); this.authToken = authToken; - this.config = Config.builder().withLogging(Logging.slf4j()) - .withMaxConnectionPoolSize(Runtime.getRuntime().availableProcessors()) - .build(); - var settings = new SecuritySettings(config.encrypted(), config.trustStrategy()); - this.securityPlan = SecurityPlans.createSecurityPlan(settings, uri.getScheme(), null, Logging.none()); + this.config = Config.builder() + .withLogging(Logging.slf4j()) + .withMaxConnectionPoolSize(Runtime.getRuntime().availableProcessors()) + .build(); + var settings = new SecuritySettings(this.config.encrypted(), this.config.trustStrategy()); + this.securityPlan = SecurityPlans.createSecurityPlan(settings, this.uri.getScheme(), null, Logging.none()); this.driverFactory = new DriverFactory(); } /** - * This method asserts that the current driver instance is usable before handing it out. If it isn't usable, it - * creates a new one. - * - * @return A shared driver instance, connected to either a database running inside test containers or - * running locally. + * A driver is usable if it's not null and can verify its connectivity. This + * method force closes the bean if the connectivity cannot be verified to avoid + * having a netty pool dangling around. + * @param driver The driver that should be checked for usability + * @return true if the driver is currently usable. + */ + private static boolean isUsable(Driver driver) { + + if (driver == null) { + return false; + } + try { + driver.verifyConnectivity(); + return true; + } + catch (Exception ex) { + try { + driver.close(); + } + catch (Exception nested) { + } + return false; + } + } + + /** + * This method asserts that the current driver instance is usable before handing + * it out. If it isn't usable, it creates a new one. + * @return A shared driver instance, connected to either a database running inside + * test containers or running locally. */ public Driver getDriver() { @@ -223,31 +275,8 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { } private Driver createDriverInstance() { - return this.driverFactory.newInstance(uri, AuthTokenManagers.basic(() -> authToken), null, config, securityPlan, EVENT_LOOP_GROUP, null); - } - - /** - * A driver is usable if it's not null and can verify its connectivity. This method force closes - * the bean if the connectivity cannot be verified to avoid having a netty pool dangling around. - * - * @param driver The driver that should be checked for usability - * @return true if the driver is currently usable. - */ - private static boolean isUsable(Driver driver) { - - if (driver == null) { - return false; - } - try { - driver.verifyConnectivity(); - return true; - } catch (Exception ex) { - try { - driver.close(); - } catch (Exception nested) { - } - return false; - } + return this.driverFactory.newInstance(this.uri, AuthTokenManagers.basic(() -> this.authToken), null, + this.config, this.securityPlan, EVENT_LOOP_GROUP, null); } public ServerVersion getServerVersion() { @@ -259,14 +288,18 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { if (serverVersion == null) { String versionString = ""; try (Session session = this.getDriver().session()) { - Record result = session.run("CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version").single(); + Record result = session.run( + "CALL dbms.components() YIELD name, versions WHERE name = 'Neo4j Kernel' RETURN 'Neo4j/' + versions[0] as version") + .single(); versionString = result.get("version").asString(); this.cachedServerVersion = ServerVersion.version(versionString); - } catch (Exception e) { + } + catch (Exception ex) { if (versionString.matches("Neo4j/20\\d{2}.+")) { this.cachedServerVersion = ServerVersion.vInDev; - } else { - throw new RuntimeException("Could not determine server version", e); + } + else { + throw new RuntimeException("Could not determine server version", ex); } } serverVersion = this.cachedServerVersion; @@ -281,7 +314,11 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { String edition; SessionConfig sessionConfig = SessionConfig.builder().withDefaultAccessMode(AccessMode.READ).build(); try (Session session = getDriver().session(sessionConfig)) { - edition = session.run("CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition").single().get("edition").asString(); + edition = session + .run("CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition") + .single() + .get("edition") + .asString(); } return edition.toLowerCase(Locale.ENGLISH); } @@ -304,42 +341,48 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { public void close() { // Don't open up a driver for just closing it - if (driverInstance == null) { + if (this.driverInstance == null) { return; } - // Catch all the things... The driver has been closed maybe by a Spring Context already + // Catch all the things... The driver has been closed maybe by a Spring + // Context already try { log.debug("Closing Neo4j connection support."); - driverInstance.close(); - } catch (Exception e) { + this.driverInstance.close(); + } + catch (Exception ex) { } } + } static class ContainerAdapter implements ExtensionContext.Store.CloseableResource { - private static final String repository = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_REPOSITORY)).orElse("neo4j"); + private static final String repository = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_REPOSITORY)) + .orElse("neo4j"); - private static final String imageVersion = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_VERSION)).orElse("5"); + private static final String imageVersion = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_VERSION)) + .orElse("5"); - private static final boolean containerReuseSupported = TestcontainersConfiguration - .getInstance().environmentSupportsReuse(); + private static final boolean containerReuseSupported = TestcontainersConfiguration.getInstance() + .environmentSupportsReuse(); - private static final boolean forceReuse = Boolean.parseBoolean(System.getenv(SYS_PROPERTY_FORCE_CONTAINER_REUSE)); + private static final boolean forceReuse = Boolean + .parseBoolean(System.getenv(SYS_PROPERTY_FORCE_CONTAINER_REUSE)); private static final Neo4jContainer neo4jContainer = new Neo4jContainer<>(repository + ":" + imageVersion) - .withoutAuthentication() - .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", - Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_ACCEPT_COMMERCIAL_EDITION)).orElse("no")) - .withTmpFs(Map.of("/log", "rw", "/data", "rw")) - .withReuse(containerReuseSupported); + .withoutAuthentication() + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", + Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_ACCEPT_COMMERCIAL_EDITION)).orElse("no")) + .withTmpFs(Map.of("/log", "rw", "/data", "rw")) + .withReuse(containerReuseSupported); - public String getBoltUrl() { + String getBoltUrl() { return neo4jContainer.getBoltUrl(); } - public void start() { + void start() { if (!neo4jContainer.isRunning()) { neo4jContainer.start(); } @@ -351,5 +394,7 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { neo4jContainer.close(); } } + } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jImperativeTestConfiguration.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jImperativeTestConfiguration.java index 9a1169eee..f363e9206 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jImperativeTestConfiguration.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jImperativeTestConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.test; import org.neo4j.cypherdsl.core.renderer.Configuration; + import org.springframework.data.neo4j.config.AbstractNeo4jConfig; /** diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jIntegrationTest.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jIntegrationTest.java index e0361039f..dad21e945 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jIntegrationTest.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jIntegrationTest.java @@ -21,16 +21,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringExtension; /** - * This annotation triggers the {@link Neo4jExtension}, that provides a driver instance for Neo4j integration tests. - * The important point here is that the extension possibly dirties a Spring context by closing the driver instance, so - * it has been meta annotated with {@link DirtiesContext}. That issue happens mostly when one and the same integration - * tests is run several times via an IDE: Spring will detect that the context configuration is the same and reuse the - * old context based on contextual information from the first run. The Neo4j extension will dutiful create a new - * connection and driver instance, but Spring won't ever use it. + * This annotation triggers the {@link Neo4jExtension}, that provides a driver instance + * for Neo4j integration tests. The important point here is that the extension possibly + * dirties a Spring context by closing the driver instance, so it has been meta annotated + * with {@link DirtiesContext}. That issue happens mostly when one and the same + * integration tests is run several times via an IDE: Spring will detect that the context + * configuration is the same and reuse the old context based on contextual information + * from the first run. The Neo4j extension will dutiful create a new connection and driver + * instance, but Spring won't ever use it. * * @author Michael J. Simons */ @@ -39,4 +42,5 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith({ SpringExtension.class, Neo4jExtension.class }) @DirtiesContext public @interface Neo4jIntegrationTest { + } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jReactiveTestConfiguration.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jReactiveTestConfiguration.java index 34c0c1f0c..e54ce02c2 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jReactiveTestConfiguration.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jReactiveTestConfiguration.java @@ -16,15 +16,18 @@ package org.springframework.data.neo4j.test; import org.neo4j.cypherdsl.core.renderer.Configuration; + import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; /** * @author Gerrit Meier */ -public abstract class Neo4jReactiveTestConfiguration extends AbstractReactiveNeo4jConfig implements Neo4jTestConfiguration { +public abstract class Neo4jReactiveTestConfiguration extends AbstractReactiveNeo4jConfig + implements Neo4jTestConfiguration { @Override public Configuration cypherDslConfiguration() { return getConfiguration(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java index 11fd65387..0300b42d6 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java @@ -32,4 +32,5 @@ public interface Neo4jTestConfiguration { return Configuration.defaultConfig(); } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java b/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java index 97f469f01..9646bbacc 100644 --- a/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java +++ b/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java @@ -20,33 +20,49 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * See ServerVersion.java + * See ServerVersion.java * * @author Driver Team at Neo4j */ public final class ServerVersion { + public static final String NEO4J_PRODUCT = "Neo4j"; public static final ServerVersion v5_3_0 = new ServerVersion(NEO4J_PRODUCT, 5, 3, 0); + public static final ServerVersion v5_0_0 = new ServerVersion(NEO4J_PRODUCT, 5, 0, 0); + public static final ServerVersion v4_4_0 = new ServerVersion(NEO4J_PRODUCT, 4, 4, 0); + public static final ServerVersion v4_3_0 = new ServerVersion(NEO4J_PRODUCT, 4, 3, 0); + public static final ServerVersion v4_2_0 = new ServerVersion(NEO4J_PRODUCT, 4, 2, 0); + public static final ServerVersion v4_1_0 = new ServerVersion(NEO4J_PRODUCT, 4, 1, 0); + public static final ServerVersion v4_0_0 = new ServerVersion(NEO4J_PRODUCT, 4, 0, 0); + public static final ServerVersion v3_5_0 = new ServerVersion(NEO4J_PRODUCT, 3, 5, 0); + public static final ServerVersion v3_4_0 = new ServerVersion(NEO4J_PRODUCT, 3, 4, 0); + public static final ServerVersion vInDev = new ServerVersion(NEO4J_PRODUCT, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); private static final String NEO4J_IN_DEV_VERSION_STRING = NEO4J_PRODUCT + "/dev"; - private static final Pattern PATTERN = - Pattern.compile("([^/]+)/(\\d+)\\.(\\d+)\\.?(\\d*)([.\\-+])?([\\dA-Za-z-.]*)?"); + + private static final Pattern PATTERN = Pattern + .compile("([^/]+)/(\\d+)\\.(\\d+)\\.?(\\d*)([.\\-+])?([\\dA-Za-z-.]*)?"); private final String product; + private final int major; + private final int minor; + private final int patch; + private final String stringValue; private ServerVersion(String product, int major, int minor, int patch) { @@ -69,39 +85,20 @@ public final class ServerVersion { patch = Integer.parseInt(patchString); } return new ServerVersion(product, major, minor, patch); - } else if (server.equalsIgnoreCase(NEO4J_IN_DEV_VERSION_STRING)) { + } + else if (server.equalsIgnoreCase(NEO4J_IN_DEV_VERSION_STRING)) { return vInDev; - } else { + } + else { throw new IllegalArgumentException("Cannot parse " + server); } } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; + private static String stringValue(String product, int major, int minor, int patch) { + if (major == Integer.MAX_VALUE && minor == Integer.MAX_VALUE && patch == Integer.MAX_VALUE) { + return NEO4J_IN_DEV_VERSION_STRING; } - if (o == null || getClass() != o.getClass()) { - return false; - } - - ServerVersion that = (ServerVersion) o; - - if (!product.equals(that.product)) { - return false; - } - if (major != that.major) { - return false; - } - if (minor != that.minor) { - return false; - } - return patch == that.patch; - } - - @Override - public int hashCode() { - return Objects.hash(product, major, minor, patch); + return String.format("%s/%s.%s.%s", product, major, minor, patch); } public boolean greaterThan(ServerVersion other) { @@ -121,15 +118,15 @@ public final class ServerVersion { } private int compareTo(ServerVersion o) { - if (!product.equals(o.product)) { + if (!this.product.equals(o.product)) { throw new IllegalArgumentException( - "Comparing different products '" + product + "' with '" + o.product + "'"); + "Comparing different products '" + this.product + "' with '" + o.product + "'"); } - int c = Integer.compare(major, o.major); + int c = Integer.compare(this.major, o.major); if (c == 0) { - c = Integer.compare(minor, o.minor); + c = Integer.compare(this.minor, o.minor); if (c == 0) { - c = Integer.compare(patch, o.patch); + c = Integer.compare(this.patch, o.patch); } } @@ -137,14 +134,36 @@ public final class ServerVersion { } @Override - public String toString() { - return stringValue; + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ServerVersion that = (ServerVersion) o; + + if (!this.product.equals(that.product)) { + return false; + } + if (this.major != that.major) { + return false; + } + if (this.minor != that.minor) { + return false; + } + return this.patch == that.patch; } - private static String stringValue(String product, int major, int minor, int patch) { - if (major == Integer.MAX_VALUE && minor == Integer.MAX_VALUE && patch == Integer.MAX_VALUE) { - return NEO4J_IN_DEV_VERSION_STRING; - } - return String.format("%s/%s.%s.%s", product, major, minor, patch); + @Override + public int hashCode() { + return Objects.hash(this.product, this.major, this.minor, this.patch); } + + @Override + public String toString() { + return this.stringValue; + } + } diff --git a/src/test/java/org/springframework/data/neo4j/test/ServerVersionTest.java b/src/test/java/org/springframework/data/neo4j/test/ServerVersionTest.java deleted file mode 100644 index d2f2c14cd..000000000 --- a/src/test/java/org/springframework/data/neo4j/test/ServerVersionTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2011-2025 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.test; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * See ServerVersionTest.java - * - * @author Driver Team at Neo4j - */ -class ServerVersionTest { - @Test - void version() { - Assertions.assertEquals(ServerVersion.version("Neo4j/dev"), ServerVersion.vInDev); - Assertions.assertEquals(ServerVersion.version("Neo4j/4.0.0"), ServerVersion.v4_0_0); - } - - @Test - void shouldHaveCorrectToString() { - Assertions.assertEquals("Neo4j/dev", ServerVersion.vInDev.toString()); - Assertions.assertEquals("Neo4j/4.0.0", ServerVersion.v4_0_0.toString()); - Assertions.assertEquals("Neo4j/3.5.0", ServerVersion.v3_5_0.toString()); - Assertions.assertEquals("Neo4j/3.5.7", ServerVersion.version("Neo4j/3.5.7").toString()); - } - - @Test - void shouldFailToParseIllegalVersions() { - Assertions.assertThrows(IllegalArgumentException.class, () -> ServerVersion.version("")); - Assertions.assertThrows(IllegalArgumentException.class, () -> ServerVersion.version("/1.2.3")); - Assertions.assertThrows(IllegalArgumentException.class, () -> ServerVersion.version("Neo4j1.2.3")); - Assertions.assertThrows(IllegalArgumentException.class, () -> ServerVersion.version("Neo4j")); - } - - @Test - void shouldFailToCompareDifferentProducts() { - ServerVersion version1 = ServerVersion.version("MyNeo4j/1.2.3"); - ServerVersion version2 = ServerVersion.version("OtherNeo4j/1.2.4"); - - Assertions.assertThrows(IllegalArgumentException.class, () -> version1.greaterThanOrEqual(version2)); - } -} diff --git a/src/test/java/org/springframework/data/neo4j/test/ServerVersionTests.java b/src/test/java/org/springframework/data/neo4j/test/ServerVersionTests.java new file mode 100644 index 000000000..1226dbec0 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/test/ServerVersionTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011-2025 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.test; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * See ServerVersionTest.java + * + * @author Driver Team at Neo4j + */ +class ServerVersionTests { + + @Test + void version() { + assertThat(ServerVersion.vInDev).isEqualTo(ServerVersion.version("Neo4j/dev")); + assertThat(ServerVersion.v4_0_0).isEqualTo(ServerVersion.version("Neo4j/4.0.0")); + } + + @Test + void shouldHaveCorrectToString() { + assertThat(ServerVersion.vInDev.toString()).isEqualTo("Neo4j/dev"); + assertThat(ServerVersion.v4_0_0.toString()).isEqualTo("Neo4j/4.0.0"); + assertThat(ServerVersion.v3_5_0.toString()).isEqualTo("Neo4j/3.5.0"); + assertThat(ServerVersion.version("Neo4j/3.5.7").toString()).isEqualTo("Neo4j/3.5.7"); + } + + @Test + void shouldFailToParseIllegalVersions() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ServerVersion.version("")); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ServerVersion.version("/1.2.3")); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ServerVersion.version("Neo4j1.2.3")); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ServerVersion.version("Neo4j")); + } + + @Test + void shouldFailToCompareDifferentProducts() { + ServerVersion version1 = ServerVersion.version("MyNeo4j/1.2.3"); + ServerVersion version2 = ServerVersion.version("OtherNeo4j/1.2.4"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> version1.greaterThanOrEqual(version2)); + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/test/TestIdentitySupport.java b/src/test/java/org/springframework/data/neo4j/test/TestIdentitySupport.java index c5f8e1d07..40f060c41 100644 --- a/src/test/java/org/springframework/data/neo4j/test/TestIdentitySupport.java +++ b/src/test/java/org/springframework/data/neo4j/test/TestIdentitySupport.java @@ -22,6 +22,9 @@ import org.neo4j.driver.types.Entity; */ public final class TestIdentitySupport { + private TestIdentitySupport() { + } + /** * @param entity The entity container as received from the server. * @return The internal id @@ -31,6 +34,4 @@ public final class TestIdentitySupport { return entity.id(); } - private TestIdentitySupport() { - } } diff --git a/src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTest.java b/src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTests.java similarity index 93% rename from src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTest.java rename to src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTests.java index 484c57bd5..0399e0324 100644 --- a/src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTest.java +++ b/src/test/java/org/springframework/data/neo4j/types/GeographicPoint2dTests.java @@ -15,16 +15,17 @@ */ package org.springframework.data.neo4j.types; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class GeographicPoint2dTest { +class GeographicPoint2dTests { + @Test - public void constructorShouldSetCorrectFields() { + void constructorShouldSetCorrectFields() { double latitude = 48.793889; double longitude = 9.226944; @@ -33,4 +34,5 @@ class GeographicPoint2dTest { assertThat(geographicPoint2d.getLatitude()).isEqualTo(latitude); assertThat(geographicPoint2d.getLongitude()).isEqualTo(longitude); } + } diff --git a/src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTest.java b/src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTests.java similarity index 93% rename from src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTest.java rename to src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTests.java index c2c161aaa..6ddc6d3de 100644 --- a/src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTest.java +++ b/src/test/java/org/springframework/data/neo4j/types/GeographicPoint3dTests.java @@ -15,16 +15,17 @@ */ package org.springframework.data.neo4j.types; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ -class GeographicPoint3dTest { +class GeographicPoint3dTests { + @Test - public void constructorShouldSetCorrectFields() { + void constructorShouldSetCorrectFields() { double latitude = 48.793889; double longitude = 9.226944; @@ -35,4 +36,5 @@ class GeographicPoint3dTest { assertThat(geographicPoint.getLongitude()).isEqualTo(longitude); assertThat(geographicPoint.getHeight()).isEqualTo(elevation); } + } diff --git a/src/test/kotlin/org/springframework/data/neo4j/core/mapping/KotlinPostLoad.kt b/src/test/kotlin/org/springframework/data/neo4j/core/mapping/KotlinPostLoad.kt index 01fbd36e1..dd062974e 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/core/mapping/KotlinPostLoad.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/core/mapping/KotlinPostLoad.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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.core.mapping import org.springframework.data.neo4j.core.schema.GeneratedValue diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt index 232662196..6fe4fb34d 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt @@ -42,7 +42,6 @@ import java.util.function.Consumer /** * @author Michael J. Simons - * @soundtrack Genesis - Invisible Touch */ @Neo4jIntegrationTest class KotlinInheritanceIT @Autowired constructor( diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java b/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java index 4111cfbc9..3a58db1f3 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/k/package-info.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011-2025 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. + */ /** * Add `k` as package name as I couldn't figure out in which way the presence of KotlinIssuesIT in the split package * (between the Kotlin and Java code) messed up the application context in tests. diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/shared/common/KotlinInheritance.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/shared/common/KotlinInheritance.kt index 3bb183653..5c37ddea8 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/shared/common/KotlinInheritance.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/shared/common/KotlinInheritance.kt @@ -23,7 +23,6 @@ import org.springframework.data.neo4j.core.schema.Relationship /** * @author Michael J. Simons - * @soundtrack Genesis - Invisible Touch */ @Node abstract class AbstractKotlinBase(open val name: String) { diff --git a/src/test/resources/META-INF/neo4j-named-queries.properties b/src/test/resources/META-INF/neo4j-named-queries.properties index 6f64b6e83..4dfa58985 100644 --- a/src/test/resources/META-INF/neo4j-named-queries.properties +++ b/src/test/resources/META-INF/neo4j-named-queries.properties @@ -1,11 +1,11 @@ # -# Copyright 2011-2022 the original author or authors. -# +# Copyright 2011-2025 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 +# 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, @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # + PersonWithAllConstructor.getOptionalPersonViaNamedQuery=MATCH (n:PersonWithAllConstructor{name::#{#part1 + #part2}}) return n diff --git a/src/test/resources/data/migrations/V20__Create_Constraints.xml b/src/test/resources/data/migrations/V20__Create_Constraints.xml index 2627a4681..cdd416af6 100644 --- a/src/test/resources/data/migrations/V20__Create_Constraints.xml +++ b/src/test/resources/data/migrations/V20__Create_Constraints.xml @@ -1,4 +1,21 @@ + diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 5646e1426..632385e01 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -1,18 +1,20 @@ diff --git a/src/test/resources/more-custom-queries.properties b/src/test/resources/more-custom-queries.properties index 4bcc26293..145bdd014 100644 --- a/src/test/resources/more-custom-queries.properties +++ b/src/test/resources/more-custom-queries.properties @@ -1 +1,17 @@ +# +# Copyright 2011-2025 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. +# + Entity2328.getSomeEntityViaNamedQuery=MATCH (n:Entity2328 {name: 'A name'}) RETURN n