chore: Align formatting with Spring Framework and Spring Boot. (#3017)
We use the Spring Formatter already in many Neo4j projects and are quite happy with it. It feels natural to use it with a Spring project, too. We take the opportunity to apply the formatter to the upcoming 8.0.x release. We think that the now increased difficulty of back porting things to the 7.x lines is outweighed by the better format and the simpler tooling. Signed-off-by: Michael Simons <michael@simons.ac>
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
root=true
|
||||
|
||||
[*.java]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
continuation_indent_size = 8
|
||||
@@ -1,4 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>io.spring.develocity.conventions</groupId>
|
||||
|
||||
@@ -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).
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
16
ci/clean.sh
16
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
16
ci/test.sh
16
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
|
||||
|
||||
|
||||
@@ -1,144 +1,66 @@
|
||||
<?xml version="1.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.
|
||||
|
||||
-->
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<module name="Checker">
|
||||
<!-- Most of them took inspiration from https://github.com/spring-io/spring-javaformat/blob/500ef15dc3dabb79298968d2d323ef3c4230fc44/src/checkstyle/checkstyle.xml -->
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
||||
<module name="RegexpHeader">
|
||||
<property name="headerFile" value="${checkstyle.header.file}" />
|
||||
<property name="fileExtensions" value="java" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck">
|
||||
<property name="lineSeparator" value="lf"/>
|
||||
</module>
|
||||
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
<module name="SuppressWarningsFilter" />
|
||||
|
||||
<module name="io.spring.nohttp.checkstyle.check.NoHttpCheck">
|
||||
<!-- XML requires double escaping, config gets XML-processed twice -->
|
||||
<property name="allowlist" value="http://www\.querydsl\.com.*&#10;http://www\.prowaveconsulting\.com.*&#10;http://www\.scispike\.com.*&#10;http://.*.icu-project\.org.*" />
|
||||
</module>
|
||||
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="SuppressWarningsHolder" />
|
||||
|
||||
<!-- Coding -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.CovariantEqualsCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.EmptyStatementCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.EqualsHashCodeCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanExpressionCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanReturnCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.StringLiteralEqualityCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.NestedForDepthCheck">
|
||||
<property name="max" value="3" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.NestedIfDepthCheck">
|
||||
<property name="max" value="4" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.NestedTryDepthCheck">
|
||||
<property name="max" value="3" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.MultipleVariableDeclarationsCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.RequireThisCheck">
|
||||
<property name="checkMethods" value="false" />
|
||||
<property name="validateOnlyOverlapping" value="true" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.HiddenFieldCheck">
|
||||
<property name="ignoreConstructorParameter" value="true" />
|
||||
<property name="ignoreSetter" value="true" />
|
||||
<property name="setterCanReturnItsClass" value="true" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.coding.OneStatementPerLineCheck" />
|
||||
|
||||
<!-- Imports -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.AvoidStaticImportCheck">
|
||||
<property name="excludes"
|
||||
value="org.neo4j.cypherdsl.core.Cypher.*, org.neo4j.cypherdsl.core.Functions.*, org.neo4j.cypherdsl.core.Conditions.*, org.neo4j.cypherdsl.core.Predicates.*, org.apiguardian.api.API.Status.*, org.assertj.core.api.Assertions.*, org.assertj.core.api.Assumptions.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.ArgumentMatchers.*" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.IllegalImportCheck">
|
||||
<property name="regexp" value="true" />
|
||||
<property name="illegalPkgs"
|
||||
value="^sun.*, ^org\.apache\.commons\.(?!compress|dbcp2|lang|lang3|logging|pool2).*, ^com\.google\.common.*, ^org\.flywaydb\.core\.internal.*, ^org\.testcontainers\.shaded.*, ^org\.neo4j\.driver\.internal\.shaded.*, ^org\.slf4j.*, ^org\.jetbrains.*" />
|
||||
<property name="illegalClasses"
|
||||
value="^reactor\.core\.support\.Assert, ^org\.junit\.rules\.ExpectedException, ^org\.junit\.Test" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck">
|
||||
<property name="processJavadoc" value="true" />
|
||||
</module>
|
||||
|
||||
<!-- Block Checks -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.EmptyBlockCheck">
|
||||
<property name="option" value="text" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.NeedBracesCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.AvoidNestedBlocksCheck" />
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.indentation.CommentsIndentationCheck">
|
||||
<property name="tokens" value="BLOCK_COMMENT_BEGIN"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.UpperEllCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.ArrayTypeStyleCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.OuterTypeFilenameCheck" />
|
||||
|
||||
<!-- Modifiers -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.modifier.RedundantModifierCheck" />
|
||||
|
||||
<!-- Regexp -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpCheck">
|
||||
<property name="format" value="[ \t]+$" />
|
||||
<property name="illegalPattern" value="true" />
|
||||
<property name="message" value="Trailing whitespace" />
|
||||
</module>
|
||||
|
||||
<!-- Whitespace -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.GenericWhitespaceCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.MethodParamPadCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.NoWhitespaceAfterCheck" >
|
||||
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS, UNARY_PLUS, ARRAY_DECLARATOR"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.NoWhitespaceBeforeCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAfterCheck" />
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck">
|
||||
<property name="allowEmptyTypes" value="true" />
|
||||
<property name="allowEmptyConstructors" value="true" />
|
||||
<property name="allowEmptyMethods" value="true" />
|
||||
<property name="allowEmptyCatches" value="true" />
|
||||
</module>
|
||||
|
||||
<!-- We have some empty blocks and statements. -->
|
||||
<module name="SuppressionCommentFilter"/>
|
||||
|
||||
<!-- Java Doc-->
|
||||
<module name="AtclauseOrder" />
|
||||
<module name="JavadocType">
|
||||
<property name="scope" value="public"/>
|
||||
<property name="allowUnknownTags" value="true" />
|
||||
</module>
|
||||
|
||||
<module name="MissingJavadocType" />
|
||||
<module name="NonEmptyAtclauseDescription" />
|
||||
|
||||
<module name="com.puppycrawl.tools.checkstyle.Checker">
|
||||
<module name="io.spring.javaformat.checkstyle.SpringChecks">
|
||||
<property name="excludes" value="io.spring.javaformat.checkstyle.check.SpringAvoidStaticImportCheck"/>
|
||||
<property name="excludes" value="io.spring.javaformat.checkstyle.check.SpringHeaderCheck"/>
|
||||
<property name="excludes" value="io.spring.javaformat.checkstyle.check.SpringLambdaCheck"/>
|
||||
</module>
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
<module name="SuppressWarningsFilter"/>
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="SuppressWarningsHolder"/>
|
||||
<!-- System.outs -->
|
||||
<module name="Regexp">
|
||||
<property name="format" value="System\.out\.println"/>
|
||||
<property name="illegalPattern" value="true"/>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.IllegalImportCheck">
|
||||
<property name="regexp" value="true"/>
|
||||
<property name="illegalClasses"
|
||||
value="
|
||||
org.jetbrains.annotations.NotNull,
|
||||
org.jetbrains.annotations.Nullable,
|
||||
org.springframework.lang.NonNull,
|
||||
org.springframework.lang.Nullable,
|
||||
org.springframework.lang.NonNullApi,
|
||||
org.springframework.lang.NonNullFields
|
||||
"/>
|
||||
</module>
|
||||
|
||||
<module name="io.spring.javaformat.checkstyle.check.SpringAvoidStaticImportCheck">
|
||||
<property
|
||||
name="excludes"
|
||||
value="
|
||||
com.github.stefanbirkner.systemlambda.SystemLambda.*,
|
||||
com.tngtech.archunit.base.DescribedPredicate.*,
|
||||
com.tngtech.archunit.core.domain.JavaClass.Predicates.*,
|
||||
com.tngtech.archunit.lang.conditions.ArchPredicates.*,
|
||||
com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*,
|
||||
org.neo4j.cypherdsl.core.Cypher.*,
|
||||
org.apiguardian.api.API.Status.*
|
||||
"
|
||||
/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
@@ -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 .+;$
|
||||
^.*$
|
||||
@@ -1,7 +1,23 @@
|
||||
<?xml version="1.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.
|
||||
|
||||
-->
|
||||
<!DOCTYPE suppressions PUBLIC
|
||||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress checks="RegexpHeader" files="package-info\.java"/>
|
||||
</suppressions>
|
||||
|
||||
13
etc/license.tpl
Normal file
13
etc/license.tpl
Normal file
@@ -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.
|
||||
@@ -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 \
|
||||
|
||||
481
pom.xml
481
pom.xml
@@ -1,19 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
| 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.
|
||||
--><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
@@ -73,7 +76,7 @@
|
||||
<archunit.version>0.23.1</archunit.version>
|
||||
<blockhound.version>1.0.8.RELEASE</blockhound.version>
|
||||
<checkstyle.skip>${skipTests}</checkstyle.skip>
|
||||
<checkstyle.version>8.40</checkstyle.version>
|
||||
<checkstyle.version>10.20.1</checkstyle.version>
|
||||
<cypher-dsl.version>2024.5.1</cypher-dsl.version>
|
||||
<dist.id>spring-data-neo4j</dist.id>
|
||||
<dist.key>SDNEO4J</dist.key>
|
||||
@@ -89,6 +92,8 @@
|
||||
<jsr305.version>3.0.2</jsr305.version>
|
||||
<junit-cc-testcontainer>2021.0.1</junit-cc-testcontainer>
|
||||
<junit-pioneer.version>2.2.0</junit-pioneer.version>
|
||||
<license-maven-plugin.version>5.0.0</license-maven-plugin.version>
|
||||
<maven-checkstyle-plugin.version>3.6.0</maven-checkstyle-plugin.version>
|
||||
<maven-install-plugin.version>3.1.4</maven-install-plugin.version>
|
||||
<maven-site-plugin.version>3.7.1</maven-site-plugin.version>
|
||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
@@ -103,14 +108,35 @@
|
||||
<reactive-streams.version>1.2.1</reactive-streams.version>
|
||||
<skipArchitectureTests>true</skipArchitectureTests>
|
||||
<skipIntegrationTests>${skipTests}</skipIntegrationTests>
|
||||
|
||||
<skipUnitTests>${skipTests}</skipUnitTests>
|
||||
|
||||
<sortpom-maven-plugin.version>4.0.0</sortpom-maven-plugin.version>
|
||||
<spring-javaformat.version>0.0.46</spring-javaformat.version>
|
||||
<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-bom</artifactId>
|
||||
<version>${r2dbc.releasetrain}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-cypher-dsl-bom</artifactId>
|
||||
<version>${cypher-dsl.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
@@ -121,24 +147,11 @@
|
||||
<artifactId>archunit</artifactId>
|
||||
<version>${archunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>junit-jupiter-causal-cluster-testcontainer-extension</artifactId>
|
||||
<version>${junit-cc-testcontainer}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.tools</groupId>
|
||||
<artifactId>blockhound</artifactId>
|
||||
<version>${blockhound.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-bom</artifactId>
|
||||
<version>${r2dbc.releasetrain}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
@@ -149,12 +162,6 @@
|
||||
<artifactId>jakarta.interceptor-api</artifactId>
|
||||
<version>${jakarta.interceptor-api.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
@@ -180,13 +187,6 @@
|
||||
<artifactId>neo4j</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-cypher-dsl-bom</artifactId>
|
||||
<version>${cypher-dsl.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j.driver</groupId>
|
||||
<artifactId>neo4j-java-driver</artifactId>
|
||||
@@ -218,108 +218,36 @@
|
||||
<version>${springdata.commons}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>junit-jupiter-causal-cluster-testcontainer-extension</artifactId>
|
||||
<version>${junit-cc-testcontainer}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- Including this explicitly in test scope makes
|
||||
[WARNING] unknown enum constant When.MAYBE
|
||||
[WARNING] reason: class file for javax.annotation.meta.When not found
|
||||
go away
|
||||
-->
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.querydsl</groupId>
|
||||
<artifactId>querydsl-core</artifactId>
|
||||
<version>${querydsl}</version>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>annotations</artifactId>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>junit-jupiter-causal-cluster-testcontainer-extension</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>neo4j-migrations</artifactId>
|
||||
<version>${neo4j-migrations.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.mockk</groupId>
|
||||
<artifactId>mockk-jvm</artifactId>
|
||||
<version>${mockk}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.tools</groupId>
|
||||
<artifactId>blockhound</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.enterprise</groupId>
|
||||
<artifactId>jakarta.enterprise.cdi-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.transaction</groupId>
|
||||
<artifactId>jakarta.transaction-api</artifactId>
|
||||
<version>${jakarta.transaction-api.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.openwebbeans</groupId>
|
||||
<artifactId>openwebbeans-se</artifactId>
|
||||
<version>${webbeans}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apiguardian</groupId>
|
||||
<artifactId>apiguardian-api</artifactId>
|
||||
@@ -332,13 +260,13 @@
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<optional>true</optional>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>annotations</artifactId>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
@@ -355,11 +283,6 @@
|
||||
<artifactId>kotlinx-coroutines-reactor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-cypher-dsl</artifactId>
|
||||
@@ -392,6 +315,88 @@
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.querydsl</groupId>
|
||||
<artifactId>querydsl-core</artifactId>
|
||||
<version>${querydsl}</version>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.enterprise</groupId>
|
||||
<artifactId>jakarta.enterprise.cdi-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- Including this explicitly in test scope makes
|
||||
[WARNING] unknown enum constant When.MAYBE
|
||||
[WARNING] reason: class file for javax.annotation.meta.When not found
|
||||
go away
|
||||
-->
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>junit-jupiter-causal-cluster-testcontainer-extension</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>neo4j-migrations</artifactId>
|
||||
<version>${neo4j-migrations.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.mockk</groupId>
|
||||
<artifactId>mockk-jvm</artifactId>
|
||||
<version>${mockk}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.tools</groupId>
|
||||
<artifactId>blockhound</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.openwebbeans</groupId>
|
||||
<artifactId>openwebbeans-se</artifactId>
|
||||
<version>${webbeans}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-r2dbc</artifactId>
|
||||
@@ -404,8 +409,8 @@
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>annotations</artifactId>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
@@ -421,16 +426,16 @@
|
||||
<!-- Exclusion because there is a Spring Data parent dependency to a newer JUnit version -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>junit</artifactId>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>annotations</artifactId>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>lombok</artifactId>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
@@ -459,25 +464,83 @@
|
||||
<plugin>
|
||||
<groupId>com.github.ekryd.sortpom</groupId>
|
||||
<artifactId>sortpom-maven-plugin</artifactId>
|
||||
<version>2.12.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sort</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<version>${sortpom-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<keepBlankLines>true</keepBlankLines>
|
||||
<nrOfIndentSpace>-1</nrOfIndentSpace>
|
||||
<sortProperties>true</sortProperties>
|
||||
<sortDependencies>groupId,artifactId</sortDependencies>
|
||||
<sortDependencies>scope,groupId,artifactId</sortDependencies>
|
||||
<createBackupFile>false</createBackupFile>
|
||||
<expandEmptyElements>false</expandEmptyElements>
|
||||
<verifyFail>stop</verifyFail>
|
||||
<verifyFailOn>strict</verifyFailOn>
|
||||
<spaceBeforeCloseEmptyElement>true</spaceBeforeCloseEmptyElement>
|
||||
<pomFile>pom.xml</pomFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<version>${license-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<licenseSets>
|
||||
<licenseSet>
|
||||
<header>${project.basedir}/etc/license.tpl</header>
|
||||
<properties>
|
||||
<year>2025</year>
|
||||
</properties>
|
||||
<includes>
|
||||
<include>**</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>**/*.adoc</exclude>
|
||||
<exclude>**/*.cypher</exclude>
|
||||
<exclude>**/*.tpl</exclude>
|
||||
<exclude>**/aot.factories</exclude>
|
||||
<exclude>**/Jenkinsfile</exclude>
|
||||
<exclude>**/license.txt</exclude>
|
||||
<exclude>**/LICENSE.txt</exclude>
|
||||
<exclude>**/notice.txt</exclude>
|
||||
<exclude>**/org.mockito.plugins.MockMaker</exclude>
|
||||
<exclude>**/spring.tooling</exclude>
|
||||
</excludes>
|
||||
</licenseSet>
|
||||
</licenseSets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
<version>${spring-javaformat.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${maven-checkstyle-plugin.version}</version>
|
||||
<configuration>
|
||||
<excludes>**/module-info.java</excludes>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
<configLocation>etc/checkstyle/config.xml</configLocation>
|
||||
<suppressionsLocation>etc/checkstyle/suppressions.xml</suppressionsLocation>
|
||||
<inputEncoding>${project.build.sourceEncoding}</inputEncoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>${checkstyle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-checkstyle</artifactId>
|
||||
<version>${spring-javaformat.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
@@ -497,43 +560,56 @@
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<groupId>com.github.ekryd.sortpom</groupId>
|
||||
<artifactId>sortpom-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
<phase>validate</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<phase>validate</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>validate</goal>
|
||||
</goals>
|
||||
<phase>validate</phase>
|
||||
<inherited>true</inherited>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>verify</id>
|
||||
<phase>verify</phase>
|
||||
<id>checkstyle-validation</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<phase>validate</phase>
|
||||
<inherited>true</inherited>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration combine.self="override">
|
||||
<configLocation>${project.basedir}/etc/checkstyle/config.xml</configLocation>
|
||||
<suppressionsLocation>${project.basedir}/etc/checkstyle/suppressions.xml</suppressionsLocation>
|
||||
<headerLocation>${project.basedir}/etc/checkstyle/java-header.txt</headerLocation>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration combine.self="append">
|
||||
<tags>
|
||||
<tag>
|
||||
<name>soundtrack</name>
|
||||
<placement>X</placement>
|
||||
<head>Soundtrack</head>
|
||||
</tag>
|
||||
</tags>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
@@ -571,10 +647,10 @@
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<phase>validate</phase>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireMavenVersion>
|
||||
@@ -601,6 +677,9 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>${skipIntegrationTests}</skipTests>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -609,33 +688,30 @@
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<skipTests>${skipIntegrationTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten.clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>resolveCiFriendliesOnly</flattenMode>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
<phase>process-resources</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten.clean</id>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
<phase>clean</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@@ -710,6 +786,25 @@
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>fast</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>fast</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<asciidoctor.skip>true</asciidoctor.skip>
|
||||
<checkstyle.skip>true</checkstyle.skip>
|
||||
<docker.skip>true</docker.skip>
|
||||
<jacoco.skip>true</jacoco.skip>
|
||||
<license.skip>true</license.skip>
|
||||
<maven.javadoc.skip>true</maven.javadoc.skip>
|
||||
<skipTests>true</skipTests>
|
||||
<sort.skip>true</sort.skip>
|
||||
<spring-javaformat.skip>true</spring-javaformat.skip>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
||||
17
settings.xml
17
settings.xml
@@ -1,3 +1,20 @@
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
# PACKAGES antora@3.2.0-alpha.2 @antora/atlas-extension:1.0.0-alpha.1 @antora/collector-extension@1.0.0-alpha.3 @springio/antora-extensions@1.1.0-alpha.2 @asciidoctor/tabs@1.0.0-alpha.12 @opendevise/antora-release-line-extension@1.0.0-alpha.2
|
||||
#
|
||||
# The purpose of this Antora playbook is to build the docs in the current branch.
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
name: data-neo4j
|
||||
version: true
|
||||
title: Spring Data Neo4j
|
||||
|
||||
@@ -135,7 +135,21 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
|
||||
include::example$documentation/Neo4jConfig.java[tags=faq.multidatabase]
|
||||
@Configuration
|
||||
public class Neo4jConfig {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
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<PersonEntity> 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 <<faq.path-mapping,path mapping>>.)
|
||||
@@ -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<String> 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<ThingWithAssignedId> nameChanger() {
|
||||
return entity -> {
|
||||
ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId(),
|
||||
entity.getName() + " (Edited)");
|
||||
return updatedThing;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
|
||||
return (entity, definition, source) -> {
|
||||
entity.setRandomValue(UUID.randomUUID().toString());
|
||||
return entity;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
No additional configuration is required.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
/**
|
||||
* <!-- tag::intent[] -->
|
||||
This package contains configuration related support classes that can be used for application specific, annotated
|
||||
|
||||
@@ -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<Inheritance.ParentModel> 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<String> roles;
|
||||
|
||||
@TargetNode
|
||||
private final PersonEntity person;
|
||||
|
||||
public Roles(PersonEntity person, List<String> roles) {
|
||||
this.person = person;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
|
||||
public List<String> 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<Roles> actorsAndRoles = new ArrayList<>();
|
||||
----
|
||||
|
||||
[[mapping.annotations.relationship.remarks]]
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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<Class<?>> IS_SIMPLE_TYPE = Neo4jSimpleTypes.HOLDER::isSimpleType;
|
||||
|
||||
private Neo4jAotPredicates() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends Class<?>> 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<Class<?>> action) {
|
||||
delegate.forEach(action);
|
||||
this.delegate.forEach(action);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Neo4jBookmarkManager> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Neo4jBookmarkManager> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>{@link org.springframework.data.neo4j.core.convert.Neo4jConversions}</li>
|
||||
* <li>{@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}</li>
|
||||
* <li>{@link org.springframework.data.neo4j.core.Neo4jOperations}</li>
|
||||
* <li>{@link org.springframework.data.neo4j.core.convert.Neo4jConversions}</li>
|
||||
* <li>{@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}</li>
|
||||
* <li>{@link org.springframework.data.neo4j.core.Neo4jOperations}</li>
|
||||
* </ul>
|
||||
* 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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends Annotation> 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> T resolve(Instance<T> 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> neo4jClient,
|
||||
@Any Instance<Neo4jMappingContext> mappingContext,
|
||||
@Any Instance<Configuration> cypherDslConfiguration,
|
||||
@Any Instance<PlatformTransactionManager> transactionManager
|
||||
) {
|
||||
@Produces
|
||||
@Builtin
|
||||
@Singleton
|
||||
public Neo4jOperations neo4jOperations(@Any Instance<Neo4jClient> neo4jClient,
|
||||
@Any Instance<Neo4jMappingContext> mappingContext, @Any Instance<Configuration> cypherDslConfiguration,
|
||||
@Any Instance<PlatformTransactionManager> 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> neo4JConversions) {
|
||||
@Produces
|
||||
@Singleton
|
||||
public Neo4jMappingContext neo4jMappingContext(@SuppressWarnings("CdiInjectionPointsInspection") Driver driver,
|
||||
@Any Instance<Neo4jConversions> 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<DatabaseSelectionProvider> databaseNameProvider) {
|
||||
@SuppressWarnings("CdiInjectionPointsInspection") Driver driver,
|
||||
@Any Instance<DatabaseSelectionProvider> databaseNameProvider) {
|
||||
|
||||
return new Neo4jTransactionManager(driver, resolve(databaseNameProvider));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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> DEFAULT_BEAN = new AnnotationLiteral<Default>() {
|
||||
@Override public Class<? extends Annotation> annotationType() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return Default.class;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An annotation literal used for selecting {@link Any @Any} annotated beans.
|
||||
*/
|
||||
public static final AnnotationLiteral<Any> ANY_BEAN = new AnnotationLiteral<Any>() {
|
||||
@Override public Class<? extends Annotation> annotationType() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return Any.class;
|
||||
}
|
||||
};
|
||||
@@ -87,15 +92,12 @@ public final class Neo4jCdiExtension extends CdiRepositoryExtensionSupport {
|
||||
Class<?> repositoryType = entry.getKey();
|
||||
Set<Annotation> qualifiers = entry.getValue();
|
||||
|
||||
Neo4jRepositoryFactoryCdiBean<?> repositoryBean = new Neo4jRepositoryFactoryCdiBean<>(
|
||||
qualifiers,
|
||||
repositoryType,
|
||||
beanManager,
|
||||
optionalCustomRepositoryImplementationDetector
|
||||
);
|
||||
Neo4jRepositoryFactoryCdiBean<?> repositoryBean = new Neo4jRepositoryFactoryCdiBean<>(qualifiers,
|
||||
repositoryType, beanManager, optionalCustomRepositoryImplementationDetector);
|
||||
|
||||
registerBean(repositoryBean);
|
||||
event.addBean(repositoryBean);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
|
||||
|
||||
return Neo4jEntityScanner.get().scan(getMappingBasePackages());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Class<?>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<? extends Annotation> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
/**
|
||||
* <!-- tag::intent[] -->
|
||||
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.
|
||||
* <!-- end::intent[] -->
|
||||
* <!-- tag::intent[] --> 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. <!-- end::intent[]
|
||||
* -->
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.data.neo4j.config;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Bookmark> lastBookmarks = bookmarkManager.resolve().getBookmarks();
|
||||
QueryRunner queryRunner = Neo4jTransactionManager.retrieveTransaction(this.driver, databaseSelection,
|
||||
impersonatedUser);
|
||||
Collection<Bookmark> 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<Bookmark> usedBookmarks;
|
||||
private final BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer;
|
||||
|
||||
private DelegatingQueryRunner(QueryRunner delegate, Collection<Bookmark> lastBookmarks, BiConsumer<Collection<Bookmark>, Collection<Bookmark>> 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<String, Object> 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<Bookmark> usedBookmarks;
|
||||
|
||||
private final BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer;
|
||||
|
||||
private DelegatingQueryRunner(QueryRunner delegate, Collection<Bookmark> lastBookmarks,
|
||||
BiConsumer<Collection<Bookmark>, Collection<Bookmark>> 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<String, Object> 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<String> cypherSupplier;
|
||||
|
||||
private final NamedParameters parameters;
|
||||
|
||||
RunnableStatement(Supplier<String> cypherSupplier) {
|
||||
this(cypherSupplier, new NamedParameters());
|
||||
}
|
||||
@@ -192,60 +248,21 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
private final Supplier<String> 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 <T> MappingSpec<T> fetchAs(Class<T> 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<Map<String, Object>> 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<String, Object> parameters) {
|
||||
return DefaultRunnableSpec.this.bindAll(parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRunnableSpecBoundToUser implements RunnableSpecBoundToUser {
|
||||
@@ -406,7 +429,9 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
|
||||
public RunnableSpec bindAll(Map<String, Object> parameters) {
|
||||
return DefaultRunnableSpec.this.bindAll(parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRecordFetchSpec<T> implements RecordFetchSpec<T>, MappingSpec<T> {
|
||||
@@ -419,10 +444,8 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
|
||||
|
||||
private BiFunction<TypeSystem, Record, T> mappingFunction;
|
||||
|
||||
DefaultRecordFetchSpec(DatabaseSelection databaseSelection,
|
||||
UserSelection impersonatedUser,
|
||||
RunnableStatement runnableStatement,
|
||||
BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
DefaultRecordFetchSpec(DatabaseSelection databaseSelection, UserSelection impersonatedUser,
|
||||
RunnableStatement runnableStatement, BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
|
||||
this.databaseSelection = databaseSelection;
|
||||
this.impersonatedUser = impersonatedUser;
|
||||
@@ -442,16 +465,18 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
|
||||
public Optional<T> one() {
|
||||
|
||||
try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
Optional<T> optionalValue = result.hasNext() ?
|
||||
Optional.ofNullable(mappingFunction.apply(TypeSystem.getDefault(), result.single())) :
|
||||
Optional.empty();
|
||||
Result result = this.runnableStatement.runWith(statementRunner);
|
||||
Optional<T> 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<T> first() {
|
||||
|
||||
try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
Optional<T> optionalValue = result.stream().map(partialMappingFunction(TypeSystem.getDefault())).filter(Objects::nonNull).findFirst();
|
||||
Result result = this.runnableStatement.runWith(statementRunner);
|
||||
Optional<T> 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<T> all() {
|
||||
|
||||
try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
Result result = this.runnableStatement.runWith(statementRunner);
|
||||
Collection<T> 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<T>) 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<T>) 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<Record, T> partialMappingFunction(TypeSystem typeSystem) {
|
||||
return r -> mappingFunction.apply(typeSystem, r);
|
||||
return r -> this.mappingFunction.apply(typeSystem, r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRunnableDelegation<T> implements RunnableDelegation<T>, OngoingDelegation<T> {
|
||||
|
||||
private final Function<QueryRunner, Optional<T>> callback;
|
||||
|
||||
private DatabaseSelection databaseSelection;
|
||||
|
||||
private UserSelection impersonatedUser;
|
||||
|
||||
private final Function<QueryRunner, Optional<T>> callback;
|
||||
|
||||
DefaultRunnableDelegation(Function<QueryRunner, Optional<T>> callback) {
|
||||
this.callback = callback;
|
||||
this.databaseSelection = resolveTargetDatabaseName(null);
|
||||
@@ -522,13 +554,17 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
|
||||
|
||||
@Override
|
||||
public Optional<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<DatabaseSelection> getDatabaseSelection() {
|
||||
return Mono.just(DatabaseSelection.undecided());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection) {
|
||||
public Mono<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> databaseSelection,
|
||||
Mono<UserSelection> 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<Bookmark> 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<Bookmark> 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 {
|
||||
<T> Mono<T> doInQueryRunnerForMono(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection,
|
||||
Function<ReactiveQueryRunner, Mono<T>> func) {
|
||||
|
||||
private final ReactiveQueryRunner delegate;
|
||||
private final Collection<Bookmark> usedBookmarks;
|
||||
private final BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer;
|
||||
|
||||
private DelegatingQueryRunner(ReactiveQueryRunner delegate, Collection<Bookmark> lastBookmarks, BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer) {
|
||||
this.delegate = delegate;
|
||||
this.usedBookmarks = lastBookmarks;
|
||||
this.newBookmarkConsumer = newBookmarkConsumer;
|
||||
}
|
||||
|
||||
Mono<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 ReactiveSession session) {
|
||||
return Mono.fromDirect(session.close()).then().doOnSuccess(signal ->
|
||||
this.newBookmarkConsumer.accept(usedBookmarks, session.lastBookmarks()));
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Value parameters) {
|
||||
return delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Map<String, Object> parameters) {
|
||||
return delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Record parameters) {
|
||||
return delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query) {
|
||||
return delegate.run(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(Query query) {
|
||||
return delegate.run(query);
|
||||
}
|
||||
return Mono.usingWhen(getQueryRunner(databaseSelection, userSelection), func,
|
||||
runner -> ((DelegatingQueryRunner) runner).close());
|
||||
}
|
||||
|
||||
<T> Mono<T> doInQueryRunnerForMono(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection, Function<ReactiveQueryRunner, Mono<T>> func) {
|
||||
<T> Flux<T> doInStatementRunnerForFlux(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection,
|
||||
Function<ReactiveQueryRunner, Flux<T>> func) {
|
||||
|
||||
return Mono.usingWhen(getQueryRunner(databaseSelection, userSelection), func, runner -> ((DelegatingQueryRunner) runner).close());
|
||||
}
|
||||
|
||||
<T> Flux<T> doInStatementRunnerForFlux(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection, Function<ReactiveQueryRunner, Flux<T>> 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<DatabaseSelection> 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<UserSelection> 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<Bookmark> usedBookmarks;
|
||||
|
||||
private final BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer;
|
||||
|
||||
private DelegatingQueryRunner(ReactiveQueryRunner delegate, Collection<Bookmark> lastBookmarks,
|
||||
BiConsumer<Collection<Bookmark>, Collection<Bookmark>> newBookmarkConsumer) {
|
||||
this.delegate = delegate;
|
||||
this.usedBookmarks = lastBookmarks;
|
||||
this.newBookmarkConsumer = newBookmarkConsumer;
|
||||
}
|
||||
return ReactiveUserSelectionProvider.getDefaultSelectionProvider().getUserSelection();
|
||||
|
||||
Mono<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 ReactiveSession session) {
|
||||
return Mono.fromDirect(session.close())
|
||||
.then()
|
||||
.doOnSuccess(
|
||||
signal -> this.newBookmarkConsumer.accept(this.usedBookmarks, session.lastBookmarks()));
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Value parameters) {
|
||||
return this.delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Map<String, Object> parameters) {
|
||||
return this.delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query, Record parameters) {
|
||||
return this.delegate.run(query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(String query) {
|
||||
return this.delegate.run(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<ReactiveResult> run(Query query) {
|
||||
return this.delegate.run(query);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRunnableSpec implements UnboundRunnableSpec, RunnableSpecBoundToDatabaseAndUser {
|
||||
|
||||
private final Supplier<String> cypherSupplier;
|
||||
|
||||
private final NamedParameters parameters = new NamedParameters();
|
||||
|
||||
private Mono<DatabaseSelection> databaseSelection;
|
||||
|
||||
private Mono<UserSelection> userSelection;
|
||||
|
||||
private final NamedParameters parameters = new NamedParameters();
|
||||
|
||||
DefaultRunnableSpec(Supplier<String> cypherSupplier) {
|
||||
this.databaseSelection = resolveTargetDatabaseName(null);
|
||||
this.userSelection = resolveUser(null);
|
||||
@@ -256,20 +290,24 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
@Override
|
||||
public <R> MappingSpec<R> fetchAs(Class<R> 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<Map<String, Object>> 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<ResultSummary> 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<T> implements Neo4jClient.OngoingBindSpec<T, RunnableSpec> {
|
||||
@@ -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<String, Object> newParameters) {
|
||||
return DefaultRunnableSpec.this.bindAll(newParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRunnableSpecBoundToUser implements RunnableSpecBoundToUser {
|
||||
@@ -364,7 +405,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
public RunnableSpec bindAll(Map<String, Object> newParameters) {
|
||||
return DefaultRunnableSpec.this.bindAll(newParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultRecordFetchSpec<T> implements RecordFetchSpec<T>, MappingSpec<T> {
|
||||
@@ -379,7 +422,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
|
||||
private BiFunction<TypeSystem, Record, T> mappingFunction;
|
||||
|
||||
DefaultRecordFetchSpec(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection, Supplier<String> cypherSupplier, NamedParameters parameters, BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
DefaultRecordFetchSpec(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection,
|
||||
Supplier<String> cypherSupplier, NamedParameters parameters,
|
||||
BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
|
||||
this.databaseSelection = databaseSelection;
|
||||
this.userSelection = userSelection;
|
||||
@@ -389,7 +434,8 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordFetchSpec<T> mappedBy(@SuppressWarnings("HiddenField") BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
public RecordFetchSpec<T> mappedBy(
|
||||
@SuppressWarnings("HiddenField") BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
|
||||
this.mappingFunction = mappingFunction;
|
||||
return this;
|
||||
@@ -397,81 +443,79 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
|
||||
Mono<Tuple2<String, Map<String, Object>>> 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<T> executeWith(Tuple2<String, Map<String, Object>> 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<T>) 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<T>) 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<T> 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<T> 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<T> 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<ResultSummary> 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<T> implements RunnableDelegation<T>, OngoingDelegation<T> {
|
||||
|
||||
private final Function<ReactiveQueryRunner, Mono<T>> callback;
|
||||
|
||||
private Mono<DatabaseSelection> databaseSelection;
|
||||
private final Mono<UserSelection> userSelection;
|
||||
|
||||
private Mono<DatabaseSelection> databaseSelection;
|
||||
|
||||
DefaultRunnableDelegation(Function<ReactiveQueryRunner, Mono<T>> callback) {
|
||||
this.callback = callback;
|
||||
this.databaseSelection = resolveTargetDatabaseName(null);
|
||||
@@ -488,7 +532,9 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
|
||||
@Override
|
||||
public Mono<T> run() {
|
||||
|
||||
return doInQueryRunnerForMono(databaseSelection, userSelection, callback);
|
||||
return doInQueryRunnerForMono(this.databaseSelection, this.userSelection, this.callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<UserSelection> getUserSelection() {
|
||||
return Mono.just(UserSelection.connectedUser());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<OngoingMatchAndUpdate> {
|
||||
private final Node rootNode;
|
||||
|
||||
private final List<String> oldLabels;
|
||||
|
||||
private final List<String> newLabels;
|
||||
|
||||
DynamicLabels(@Nullable NodeDescription<?> nodeDescription, Collection<String> oldLabels, @Nullable Collection<String> newLabels) {
|
||||
DynamicLabels(@Nullable NodeDescription<?> nodeDescription, Collection<String> oldLabels,
|
||||
@Nullable Collection<String> 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<OngoingMatchAndUpdate> {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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 <T> the domain tyoe
|
||||
* @return new instance of {@link ExecutableFind}.
|
||||
* @throws IllegalArgumentException if domainType is {@literal null}.
|
||||
*/
|
||||
<T> ExecutableFind<T> find(Class<T> 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 <T> returned type
|
||||
*/
|
||||
@@ -57,10 +60,10 @@ public interface FluentFindOperation {
|
||||
|
||||
/**
|
||||
* Get all matching elements.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
List<T> 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<T> 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<T> matching(String query, Map<String, Object> 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<T> 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<T> matching(Statement statement) {
|
||||
return matching(statement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,13 +162,13 @@ public interface FluentFindOperation {
|
||||
/**
|
||||
* Define the target type fields should be mapped to. <br />
|
||||
* Skip this step if you are anyway only interested in the original domain type.
|
||||
*
|
||||
* @param resultType must not be {@literal null}.
|
||||
* @param <R> result type.
|
||||
* @param <R> result type.
|
||||
* @return new instance of {@link FindWithProjection}.
|
||||
* @throws IllegalArgumentException if resultType is {@literal null}.
|
||||
*/
|
||||
<R> FindWithQuery<R> as(Class<R> resultType);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,5 +177,7 @@ public interface FluentFindOperation {
|
||||
* @param <T> returned type
|
||||
*/
|
||||
interface ExecutableFind<T> extends FindWithProjection<T> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> ExecutableSave<T> save(Class<T> domainType) {
|
||||
|
||||
Assert.notNull(domainType, "DomainType must not be null");
|
||||
|
||||
return new ExecutableSaveSupport<>(this.template, domainType);
|
||||
}
|
||||
|
||||
private static class ExecutableFindSupport<T>
|
||||
implements ExecutableFind<T>, FindWithProjection<T>, FindWithQuery<T>, TerminatingFind<T> {
|
||||
|
||||
private final Neo4jTemplate template;
|
||||
|
||||
private final Class<?> domainType;
|
||||
|
||||
private final Class<T> returnType;
|
||||
|
||||
@Nullable
|
||||
private final String query;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, Object> 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<T> returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> 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<T> matching(String query, Map<String, Object> 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<T> matching(Statement statement, Map<String, Object> 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<T> result = doFind(TemplateSupport.FetchType.ONE);
|
||||
if (result.isEmpty()) {
|
||||
@@ -127,21 +142,16 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
}
|
||||
|
||||
private List<T> 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 <T> ExecutableSave<T> save(Class<T> domainType) {
|
||||
|
||||
Assert.notNull(domainType, "DomainType must not be null");
|
||||
|
||||
return new ExecutableSaveSupport<>(this.template, domainType);
|
||||
}
|
||||
|
||||
private static class ExecutableSaveSupport<DT> implements ExecutableSave<DT> {
|
||||
|
||||
private final Neo4jTemplate template;
|
||||
|
||||
private final Class<DT> domainType;
|
||||
|
||||
ExecutableSaveSupport(Neo4jTemplate template, Class<DT> domainType) {
|
||||
@@ -166,7 +176,9 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
}
|
||||
|
||||
private <T> List<T> doSave(Iterable<T> instances) {
|
||||
return template.doSave(instances, domainType);
|
||||
return this.template.doSave(instances, this.domainType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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 <T> the type of the domain type
|
||||
* @return new instance of {@link ExecutableSave}.
|
||||
* @throws IllegalArgumentException if domainType is {@literal null}.
|
||||
*/
|
||||
<T> ExecutableSave<T> save(Class<T> 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 <DT> the domain type
|
||||
*/
|
||||
interface ExecutableSave<DT> {
|
||||
|
||||
/**
|
||||
* @param instance The instance to be saved
|
||||
* @param <T> 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 <T> 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> T one(T instance);
|
||||
|
||||
/**
|
||||
* @param instances The instances to be saved
|
||||
* @param <T> 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 <T> 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
|
||||
*/
|
||||
<T> List<T> all(Iterable<T> instances);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object>) value));
|
||||
} else if (Constants.NAME_OF_RELATIONSHIP_LIST_PARAM.equals(name) && value != null) {
|
||||
this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List<Map<String, Object>>) value));
|
||||
} else if (Constants.NAME_OF_ENTITY_LIST_PARAM.equals(name) && value != null) {
|
||||
this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List<Map<String, Object>>) value));
|
||||
} else {
|
||||
this.parameters.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> unwrapMapValueWrapperInListOfEntities(List<Map<String, Object>> entityList) {
|
||||
boolean requiresChange = entityList.stream().anyMatch(
|
||||
entity ->
|
||||
entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM) &&
|
||||
((Map<String, Object>) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)).values().stream()
|
||||
.anyMatch(MapValueWrapper.class::isInstance)
|
||||
);
|
||||
|
||||
if (!requiresChange) {
|
||||
return entityList;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> newEntityList = new ArrayList<>(entityList.size());
|
||||
for (Map<String, Object> entity : entityList) {
|
||||
if (entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM)) {
|
||||
Map<String, Object> newEntity = new HashMap<>(entity);
|
||||
newEntity.put(Constants.NAME_OF_PROPERTIES_PARAM, unwrapMapValueWrapper((Map<String, Object>) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)));
|
||||
newEntityList.add(newEntity);
|
||||
} else {
|
||||
newEntityList.add(entity);
|
||||
}
|
||||
}
|
||||
return newEntityList;
|
||||
}
|
||||
|
||||
private static Map<String, Object> unwrapMapValueWrapper(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object>) value));
|
||||
}
|
||||
else if (Constants.NAME_OF_RELATIONSHIP_LIST_PARAM.equals(name) && value != null) {
|
||||
this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List<Map<String, Object>>) value));
|
||||
}
|
||||
else if (Constants.NAME_OF_ENTITY_LIST_PARAM.equals(name) && value != null) {
|
||||
this.parameters.put(name, unwrapMapValueWrapperInListOfEntities((List<Map<String, Object>>) value));
|
||||
}
|
||||
else {
|
||||
this.parameters.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> unwrapMapValueWrapperInListOfEntities(List<Map<String, Object>> entityList) {
|
||||
boolean requiresChange = entityList.stream()
|
||||
.anyMatch(entity -> entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM)
|
||||
&& ((Map<String, Object>) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)).values()
|
||||
.stream()
|
||||
.anyMatch(MapValueWrapper.class::isInstance));
|
||||
|
||||
if (!requiresChange) {
|
||||
return entityList;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> newEntityList = new ArrayList<>(entityList.size());
|
||||
for (Map<String, Object> entity : entityList) {
|
||||
if (entity.containsKey(Constants.NAME_OF_PROPERTIES_PARAM)) {
|
||||
Map<String, Object> newEntity = new HashMap<>(entity);
|
||||
newEntity.put(Constants.NAME_OF_PROPERTIES_PARAM,
|
||||
unwrapMapValueWrapper((Map<String, Object>) entity.get(Constants.NAME_OF_PROPERTIES_PARAM)));
|
||||
newEntityList.add(newEntity);
|
||||
}
|
||||
else {
|
||||
newEntityList.add(entity);
|
||||
}
|
||||
}
|
||||
return newEntityList;
|
||||
}
|
||||
|
||||
Map<String, Object> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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 <T> the type of the result being produced
|
||||
* @return a single result object or an empty optional if the callback didn't produce
|
||||
* a result
|
||||
*/
|
||||
<T> OngoingDelegation<T> delegateTo(Function<QueryRunner, Optional<T>> 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<RunnableSpec> {
|
||||
|
||||
/**
|
||||
* Create a mapping for each record return to a specific type.
|
||||
* @param targetClass the class each record should be mapped to
|
||||
* @param <T> the type of the class
|
||||
* @return a mapping spec that allows specifying a mapping function
|
||||
*/
|
||||
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
|
||||
|
||||
/**
|
||||
* Fetch all records mapped into generic maps.
|
||||
* @return a fetch specification that maps into generic maps
|
||||
*/
|
||||
RecordFetchSpec<Map<String, Object>> 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 <S> this {@link BindSpec specs} own type
|
||||
* @since 6.0
|
||||
*/
|
||||
interface BindSpec<S extends BindSpec<S>> {
|
||||
|
||||
/**
|
||||
* 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 <T> type of the value
|
||||
*/
|
||||
<T> OngoingBindSpec<T, S> bind(@Nullable T value);
|
||||
|
||||
S bindAll(Map<String, Object> parameters);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ongoing bind specification.
|
||||
*
|
||||
* @param <S> this {@link OngoingBindSpec specs} own type
|
||||
* @param <T> binding value type
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingBindSpec<T, S extends BindSpec<S>> {
|
||||
|
||||
/**
|
||||
* 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<T, Map<String, Object>> binder);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Step for defining the mapping.
|
||||
*
|
||||
* @param <T> the resulting type of this mapping
|
||||
* @since 6.0
|
||||
*/
|
||||
interface MappingSpec<T> extends RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* 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<T> mappedBy(BiFunction<TypeSystem, Record, T> mappingFunction);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Final step that triggers fetching.
|
||||
*
|
||||
* @param <T> the type to which the fetched records are eventually mapped
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* Fetches exactly one record and throws an exception if there are more entries.
|
||||
* @return the one and only record
|
||||
*/
|
||||
Optional<T> one();
|
||||
|
||||
/**
|
||||
* Fetches only the first record. Returns an empty holder if there are no records.
|
||||
* @return the first record if any
|
||||
*/
|
||||
Optional<T> first();
|
||||
|
||||
/**
|
||||
* Fetches all records.
|
||||
* @return all records
|
||||
*/
|
||||
Collection<T> all();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A contract for an ongoing delegation in the selected database.
|
||||
*
|
||||
* @param <T> the type of the returned value
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* 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<T> in(String targetDatabase);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the stored callback.
|
||||
* @return the optional result of the callback that has been executed with the
|
||||
* given database
|
||||
*/
|
||||
Optional<T> 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<String> 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 <T> The type of the result being produced
|
||||
* @return A single result object or an empty optional if the callback didn't produce a result
|
||||
*/
|
||||
<T> OngoingDelegation<T> delegateTo(Function<QueryRunner, Optional<T>> 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<RunnableSpec> {
|
||||
|
||||
/**
|
||||
* Create a mapping for each record return to a specific type.
|
||||
*
|
||||
* @param targetClass The class each record should be mapped to
|
||||
* @param <T> The type of the class
|
||||
* @return A mapping spec that allows specifying a mapping function.
|
||||
*/
|
||||
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
|
||||
|
||||
/**
|
||||
* Fetch all records mapped into generic maps
|
||||
*
|
||||
* @return A fetch specification that maps into generic maps.
|
||||
*/
|
||||
RecordFetchSpec<Map<String, Object>> 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 <S> This {@link BindSpec specs} own type
|
||||
* @since 6.0
|
||||
*/
|
||||
interface BindSpec<S extends BindSpec<S>> {
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
<T> OngoingBindSpec<T, S> bind(@Nullable T value);
|
||||
|
||||
S bindAll(Map<String, Object> parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ongoing bind specification.
|
||||
*
|
||||
* @param <S> This {@link OngoingBindSpec specs} own type
|
||||
* @param <T> Binding value type
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingBindSpec<T, S extends BindSpec<S>> {
|
||||
|
||||
/**
|
||||
* 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<T, Map<String, Object>> binder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <T> The resulting type of this mapping
|
||||
* @since 6.0
|
||||
*/
|
||||
interface MappingSpec<T> extends RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* 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<T> mappedBy(BiFunction<TypeSystem, Record, T> mappingFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <T> The type to which the fetched records are eventually mapped
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* Fetches exactly one record and throws an exception if there are more entries.
|
||||
*
|
||||
* @return The one and only record.
|
||||
*/
|
||||
Optional<T> one();
|
||||
|
||||
/**
|
||||
* Fetches only the first record. Returns an empty holder if there are no records.
|
||||
*
|
||||
* @return The first record if any.
|
||||
*/
|
||||
Optional<T> first();
|
||||
|
||||
/**
|
||||
* Fetches all records.
|
||||
*
|
||||
* @return All records.
|
||||
*/
|
||||
Collection<T> all();
|
||||
}
|
||||
|
||||
/**
|
||||
* A contract for an ongoing delegation in the selected database.
|
||||
*
|
||||
* @param <T> The type of the returned value.
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* 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<T> in(String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the stored callback.
|
||||
*
|
||||
* @return The optional result of the callback that has been executed with the given database.
|
||||
*/
|
||||
Optional<T> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <em>Domain Type</em>.
|
||||
*
|
||||
* @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<String, Object> 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<String, Object> parameters);
|
||||
|
||||
/**
|
||||
* Load all entities of a given type.
|
||||
*
|
||||
* @param domainType the type of the entities. Must not be {@code null}.
|
||||
* @param <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}
|
||||
*/
|
||||
<T> List<T> findAll(Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}
|
||||
*/
|
||||
<T> List<T> findAll(Statement statement, Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> List<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Optional<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> List<T> findAll(String cypherQuery, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> List<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}
|
||||
*/
|
||||
<T> Optional<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> 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 <T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}
|
||||
* @return guaranteed to be not {@code null}
|
||||
*/
|
||||
<T> List<T> findAllById(Iterable<?> ids, Class<T> 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 <T> 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.
|
||||
*/
|
||||
<T> boolean existsById(Object id, Class<T> 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 <T> the type of the entity.
|
||||
* @return the saved instance.
|
||||
@@ -194,41 +184,42 @@ public interface Neo4jOperations {
|
||||
<T> 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).
|
||||
* <p>
|
||||
* 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 <T> 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 <T> the type of the entity.
|
||||
* @return the saved instance.
|
||||
* @since 6.3
|
||||
*/
|
||||
@Nullable
|
||||
default <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
|
||||
@Nullable default <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> 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 <T> the type of the entity.
|
||||
* @param <R> the type of the projection to be used during save.
|
||||
* @return the saved, projected instance.
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
default <T, R> R saveAs(T instance, Class<R> resultType) {
|
||||
@Nullable default <T, R> R saveAs(T instance, Class<R> 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 <T> the type of the entity.
|
||||
* @return the saved instances.
|
||||
@@ -236,27 +227,31 @@ public interface Neo4jOperations {
|
||||
<T> List<T> saveAll(Iterable<T> 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).
|
||||
* <p>
|
||||
* 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 <T> 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 <T> the type of the entity.
|
||||
* @return the saved instances.
|
||||
* @since 6.3
|
||||
*/
|
||||
default <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
|
||||
default <T> List<T> saveAllAs(Iterable<T> instances,
|
||||
BiPredicate<PropertyPath, Neo4jPersistentProperty> 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 <T> the type of the entity.
|
||||
* @param <R> 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 <T> the type of the entity.
|
||||
*/
|
||||
<T> void deleteById(Object id, Class<T> domainType);
|
||||
|
||||
<T> void deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue);
|
||||
<T> void deleteByIdWithVersion(Object id, Class<T> 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 <T> 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 <T> 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 <T> the type of the objects returned by this query.
|
||||
* @return an executable query
|
||||
*/
|
||||
<T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> 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 <T> The type of the objects returned by this query.
|
||||
* @return An executable query
|
||||
* @param queryFragmentsAndParameters fragments and parameters to construct the query
|
||||
* from
|
||||
* @param <T> the type of the objects returned by this query.
|
||||
* @return an executable query
|
||||
*/
|
||||
<T> ExecutableQuery<T> toExecutableQuery(Class<T> domainType,
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
|
||||
/**
|
||||
* An interface for controlling query execution.
|
||||
@@ -323,20 +318,26 @@ public interface Neo4jOperations {
|
||||
interface ExecutableQuery<T> {
|
||||
|
||||
/**
|
||||
* @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<T> 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<T> 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Optional<BiFunction<String, Throwable, DataAccessException>>> 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<String, Throwable, DataAccessException> defaultTranslationProvider) {
|
||||
|
||||
Optional<String> 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<String, Optional<BiFunction<String, Throwable, DataAccessException>>> 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<String, Throwable, DataAccessException> defaultTranslationProvider) {
|
||||
|
||||
Optional<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
* <p>
|
||||
* 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 <T> the type of the objects returned by this query.
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @param <T> 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<T> {
|
||||
|
||||
public static <CT> RequiredBuildStep<CT> queryFor(Class<CT> resultType) {
|
||||
return new RequiredBuildStep<>(resultType);
|
||||
}
|
||||
|
||||
private final Class<T> resultType;
|
||||
|
||||
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
|
||||
|
||||
@Nullable
|
||||
private final Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private volatile Optional<BiFunction<TypeSystem, Record, T>> lastMappingFunction = Optional.empty();
|
||||
|
||||
@@ -78,24 +79,27 @@ public final class PreparedQuery<T> {
|
||||
this.queryFragmentsAndParameters = optionalBuildSteps.queryFragmentsAndParameters;
|
||||
}
|
||||
|
||||
public static <CT> RequiredBuildStep<CT> queryFor(Class<CT> resultType) {
|
||||
return new RequiredBuildStep<>(resultType);
|
||||
}
|
||||
|
||||
public Class<T> getResultType() {
|
||||
return this.resultType;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public synchronized Optional<BiFunction<TypeSystem, Record, T>> getOptionalMappingFunction() {
|
||||
lastMappingFunction = Optional.ofNullable(this.mappingFunctionSupplier)
|
||||
.map(Supplier::get)
|
||||
.map(f -> (BiFunction<TypeSystem, Record, T>) new AggregatingMappingFunction(f));
|
||||
return lastMappingFunction;
|
||||
this.lastMappingFunction = Optional.ofNullable(this.mappingFunctionSupplier)
|
||||
.map(Supplier::get)
|
||||
.map(f -> (BiFunction<TypeSystem, Record, T>) 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<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <CT> The concrete type of this build step.
|
||||
* Step configuring the query to be used.
|
||||
*
|
||||
* @param <CT> the concrete type of this build step.
|
||||
* @since 6.0
|
||||
*/
|
||||
public static class RequiredBuildStep<CT> {
|
||||
public static final class RequiredBuildStep<CT> {
|
||||
|
||||
private final Class<CT> resultType;
|
||||
|
||||
private RequiredBuildStep(Class<CT> resultType) {
|
||||
@@ -114,22 +121,28 @@ public final class PreparedQuery<T> {
|
||||
}
|
||||
|
||||
public OptionalBuildSteps<CT> withCypherQuery(String cypherQuery) {
|
||||
return new OptionalBuildSteps<>(resultType, new QueryFragmentsAndParameters(cypherQuery));
|
||||
return new OptionalBuildSteps<>(this.resultType, new QueryFragmentsAndParameters(cypherQuery));
|
||||
}
|
||||
|
||||
public OptionalBuildSteps<CT> withQueryFragmentsAndParameters(QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
return new OptionalBuildSteps<>(resultType, queryFragmentsAndParameters);
|
||||
public OptionalBuildSteps<CT> withQueryFragmentsAndParameters(
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
return new OptionalBuildSteps<>(this.resultType, queryFragmentsAndParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <CT> The concrete type of this build step.
|
||||
* Step configuring parameters or mapping functions.
|
||||
*
|
||||
* @param <CT> the concrete type of this build step.
|
||||
* @since 6.0
|
||||
*/
|
||||
public static class OptionalBuildSteps<CT> {
|
||||
public static final class OptionalBuildSteps<CT> {
|
||||
|
||||
final Class<CT> resultType;
|
||||
|
||||
final QueryFragmentsAndParameters queryFragmentsAndParameters;
|
||||
|
||||
@Nullable
|
||||
Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
|
||||
|
||||
@@ -140,16 +153,16 @@ public final class PreparedQuery<T> {
|
||||
|
||||
/**
|
||||
* 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<CT> withParameters(@Nullable Map<String, Object> newParameters) {
|
||||
this.queryFragmentsAndParameters.setParameters(Objects.requireNonNullElseGet(newParameters, Map::of));
|
||||
return this;
|
||||
}
|
||||
|
||||
public OptionalBuildSteps<CT> usingMappingFunction(@Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> newMappingFunction) {
|
||||
public OptionalBuildSteps<CT> usingMappingFunction(
|
||||
@Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> newMappingFunction) {
|
||||
this.mappingFunctionSupplier = newMappingFunction;
|
||||
return this;
|
||||
}
|
||||
@@ -157,11 +170,13 @@ public final class PreparedQuery<T> {
|
||||
public PreparedQuery<CT> build() {
|
||||
return new PreparedQuery<>(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class AggregatingMappingFunction implements BiFunction<TypeSystem, Record, Object> {
|
||||
|
||||
private final BiFunction<TypeSystem, MapAccessor, ?> target;
|
||||
|
||||
private final AtomicBoolean aggregated = new AtomicBoolean(false);
|
||||
|
||||
AggregatingMappingFunction(BiFunction<TypeSystem, MapAccessor, ?> target) {
|
||||
@@ -173,19 +188,19 @@ public final class PreparedQuery<T> {
|
||||
if (MappingSupport.isListContainingOnly(t.LIST(), t.PATH()).test(value)) {
|
||||
return new LinkedHashSet<Object>(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<Map.Entry<String, Value>> 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<Object> result = new LinkedHashSet<>();
|
||||
Set<Value> nodes = new LinkedHashSet<>();
|
||||
Set<Value> relationships = new LinkedHashSet<>();
|
||||
|
||||
List<Path> paths = value.hasType(t.PATH())
|
||||
? Collections.singletonList(value.asPath())
|
||||
List<Path> 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<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<String, Value> 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<T> {
|
||||
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<T> {
|
||||
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<Boolean, List<Map.Entry<String, Value>>> pathValues = r.asMap(Function.identity()).entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.partitioningBy(entry -> entry.getValue().hasType(t.PATH())));
|
||||
Map<Boolean, List<Map.Entry<String, Value>>> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<PropertyFilter.ProjectedPath> getInputProperties(ResultProcessor resultProcessor, ProjectionFactory factory,
|
||||
Neo4jMappingContext mappingContext) {
|
||||
private PropertyFilterSupport() {
|
||||
}
|
||||
|
||||
public static Collection<PropertyFilter.ProjectedPath> 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<PropertyFilter.ProjectedPath> addPropertiesFrom(Class<?> domainType, Class<?> returnType,
|
||||
ProjectionFactory projectionFactory,
|
||||
Neo4jMappingContext neo4jMappingContext) {
|
||||
ProjectionFactory projectionFactory, Neo4jMappingContext neo4jMappingContext) {
|
||||
|
||||
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
|
||||
Collection<PropertyFilter.ProjectedPath> 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<PropertyFilter.ProjectedPath> filteredProperties, ProjectionPathProcessor projectionPathProcessor,
|
||||
Neo4jMappingContext mappingContext) {
|
||||
Collection<PropertyFilter.ProjectedPath> 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(".");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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 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<DatabaseSelection> getDatabaseSelection();
|
||||
|
||||
@Override
|
||||
public Mono<DatabaseSelection> getDatabaseSelection() {
|
||||
return Mono.just(DatabaseSelection.undecided());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <br />
|
||||
* 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.<br />
|
||||
* {@link ReactiveFluentFindOperation} allows creation and execution of Neo4j find
|
||||
* operations in a fluent API style. <br />
|
||||
* 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.<br />
|
||||
*
|
||||
* @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 <T> the domain type
|
||||
* @return new instance of {@link ExecutableFind}.
|
||||
* @throws IllegalArgumentException if domainType is {@literal null}.
|
||||
*/
|
||||
<T> ExecutableFind<T> find(Class<T> 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 <T> returned type
|
||||
*/
|
||||
@@ -57,10 +58,10 @@ public interface ReactiveFluentFindOperation {
|
||||
|
||||
/**
|
||||
* Get all matching elements.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
Flux<T> 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<T> 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<T> matching(String query, Map<String, Object> 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<T> 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<T> matching(Statement statement) {
|
||||
return matching(statement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,13 +150,13 @@ public interface ReactiveFluentFindOperation {
|
||||
/**
|
||||
* Define the target type fields should be mapped to. <br />
|
||||
* Skip this step if you are anyway only interested in the original domain type.
|
||||
*
|
||||
* @param resultType must not be {@literal null}.
|
||||
* @param <R> result type.
|
||||
* @param <R> result type.
|
||||
* @return new instance of {@link FindWithProjection}.
|
||||
* @throws IllegalArgumentException if resultType is {@literal null}.
|
||||
*/
|
||||
<R> FindWithQuery<R> as(Class<R> resultType);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,5 +165,7 @@ public interface ReactiveFluentFindOperation {
|
||||
* @param <T> returned type
|
||||
*/
|
||||
interface ExecutableFind<T> extends FindWithProjection<T> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> ExecutableSave<T> save(Class<T> domainType) {
|
||||
Assert.notNull(domainType, "DomainType must not be null");
|
||||
|
||||
return new ExecutableSaveSupport<>(this.template, domainType);
|
||||
}
|
||||
|
||||
private static class ExecutableFindSupport<T>
|
||||
implements ExecutableFind<T>, FindWithProjection<T>, FindWithQuery<T>, TerminatingFind<T> {
|
||||
|
||||
private final ReactiveNeo4jTemplate template;
|
||||
|
||||
private final Class<?> domainType;
|
||||
|
||||
private final Class<T> returnType;
|
||||
|
||||
@Nullable
|
||||
private final String query;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, Object> parameters;
|
||||
|
||||
@Nullable
|
||||
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
|
||||
|
||||
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String query,
|
||||
@Nullable Map<String, Object> parameters) {
|
||||
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType,
|
||||
@Nullable String query, @Nullable Map<String, Object> 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<T> returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> 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<T> matching(QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
|
||||
return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters);
|
||||
return new ExecutableFindSupport<>(this.template, this.domainType, this.returnType,
|
||||
queryFragmentsAndParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminatingFind<T> matching(Statement statement, Map<String, Object> 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<T> 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 <T> ExecutableSave<T> save(Class<T> domainType) {
|
||||
Assert.notNull(domainType, "DomainType must not be null");
|
||||
|
||||
return new ExecutableSaveSupport<>(this.template, domainType);
|
||||
}
|
||||
|
||||
private static class ExecutableSaveSupport<DT> implements ReactiveFluentSaveOperation.ExecutableSave<DT> {
|
||||
|
||||
private final ReactiveNeo4jTemplate template;
|
||||
|
||||
private final Class<DT> domainType;
|
||||
|
||||
ExecutableSaveSupport(ReactiveNeo4jTemplate template, Class<DT> domainType) {
|
||||
@@ -157,7 +166,9 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
|
||||
}
|
||||
|
||||
private <T> Flux<T> doSave(Iterable<T> instances) {
|
||||
return template.doSave(instances, domainType);
|
||||
return this.template.doSave(instances, this.domainType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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 <T> the domain type
|
||||
* @return new instance of {@link ExecutableSave}.
|
||||
* @throws IllegalArgumentException if domainType is {@literal null}.
|
||||
*/
|
||||
<T> ExecutableSave<T> save(Class<T> 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 <DT> the domain type
|
||||
*/
|
||||
interface ExecutableSave<DT> {
|
||||
|
||||
/**
|
||||
* @param instance The instance to be saved
|
||||
* @param <T> 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 <T> 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> Mono<T> one(T instance);
|
||||
|
||||
/**
|
||||
* @param instances The instances to be saved
|
||||
* @param <T> 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 <T> 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
|
||||
*/
|
||||
<T> Flux<T> all(Iterable<T> instances);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ReactiveQueryRunner> 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<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> 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<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> databaseSelection,
|
||||
Mono<UserSelection> 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<String> 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 <T> the type of the result being produced
|
||||
* @return a single publisher containing none or exactly one element that will be
|
||||
* produced by the callback
|
||||
*/
|
||||
<T> OngoingDelegation<T> delegateTo(Function<ReactiveQueryRunner, Mono<T>> callback);
|
||||
|
||||
/**
|
||||
* Returns the assigned database selection provider.
|
||||
* @return the database selection provider - can be null
|
||||
*/
|
||||
@Nullable ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider();
|
||||
|
||||
/**
|
||||
* Step for defining the mapping.
|
||||
*
|
||||
* @param <T> the resulting type of this mapping
|
||||
* @since 6.0
|
||||
*/
|
||||
interface MappingSpec<T> extends RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* 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<T> mappedBy(BiFunction<TypeSystem, Record, T> mappingFunction);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Final step that triggers fetching.
|
||||
*
|
||||
* @param <T> the type to which the fetched records are eventually mapped
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* Fetches exactly one record and throws an exception if there are more entries.
|
||||
* @return the one and only record.
|
||||
*/
|
||||
Mono<T> one();
|
||||
|
||||
/**
|
||||
* Fetches only the first record. Returns an empty holder if there are no records.
|
||||
* @return the first record if any.
|
||||
*/
|
||||
Mono<T> first();
|
||||
|
||||
/**
|
||||
* Fetches all records.
|
||||
* @return all records.
|
||||
*/
|
||||
Flux<T> 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<RunnableSpec> {
|
||||
|
||||
/**
|
||||
* Create a mapping for each record return to a specific type.
|
||||
* @param targetClass the class each record should be mapped to
|
||||
* @param <T> the type of the class
|
||||
* @return a mapping spec that allows specifying a mapping function
|
||||
*/
|
||||
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
|
||||
|
||||
/**
|
||||
* Fetch all records mapped into generic maps.
|
||||
* @return a fetch specification that maps into generic maps
|
||||
*/
|
||||
RecordFetchSpec<Map<String, Object>> 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<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 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 <T> the type of the returned value.
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* 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<T> in(String targetDatabase);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned by the query
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the stored callback.
|
||||
* @return the optional result of the callback that has been executed with the
|
||||
* given database.
|
||||
*/
|
||||
Mono<T> 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<ReactiveQueryRunner> getQueryRunner() {
|
||||
return getQueryRunner(Mono.just(DatabaseSelection.undecided()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A managed query runner
|
||||
* @see #getQueryRunner(Mono, Mono)
|
||||
* @since 6.2
|
||||
*/
|
||||
default Mono<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> 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<ReactiveQueryRunner> getQueryRunner(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> 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<String> 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 <T> The type of the result being produced
|
||||
* @return A single publisher containing none or exactly one element that will be produced by the callback
|
||||
*/
|
||||
<T> OngoingDelegation<T> delegateTo(Function<ReactiveQueryRunner, Mono<T>> callback);
|
||||
|
||||
/**
|
||||
* Returns the assigned database selection provider.
|
||||
*
|
||||
* @return The database selection provider - can be null
|
||||
*/
|
||||
@Nullable
|
||||
ReactiveDatabaseSelectionProvider getDatabaseSelectionProvider();
|
||||
|
||||
/**
|
||||
* @param <T> The resulting type of this mapping
|
||||
* @since 6.0
|
||||
*/
|
||||
interface MappingSpec<T> extends RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* 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<T> mappedBy(BiFunction<TypeSystem, Record, T> mappingFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <T> The type to which the fetched records are eventually mapped
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RecordFetchSpec<T> {
|
||||
|
||||
/**
|
||||
* Fetches exactly one record and throws an exception if there are more entries.
|
||||
*
|
||||
* @return The one and only record.
|
||||
*/
|
||||
Mono<T> one();
|
||||
|
||||
/**
|
||||
* Fetches only the first record. Returns an empty holder if there are no records.
|
||||
*
|
||||
* @return The first record if any.
|
||||
*/
|
||||
Mono<T> first();
|
||||
|
||||
/**
|
||||
* Fetches all records.
|
||||
*
|
||||
* @return All records.
|
||||
*/
|
||||
Flux<T> 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<RunnableSpec> {
|
||||
|
||||
/**
|
||||
* Create a mapping for each record return to a specific type.
|
||||
*
|
||||
* @param targetClass The class each record should be mapped to
|
||||
* @param <T> The type of the class
|
||||
* @return A mapping spec that allows specifying a mapping function
|
||||
*/
|
||||
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
|
||||
|
||||
/**
|
||||
* Fetch all records mapped into generic maps
|
||||
*
|
||||
* @return A fetch specification that maps into generic maps
|
||||
*/
|
||||
RecordFetchSpec<Map<String, Object>> 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<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 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 <T> The type of the returned value.
|
||||
* @since 6.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* 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<T> in(String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned by the query
|
||||
* @since 6.0
|
||||
*/
|
||||
interface RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the stored callback.
|
||||
*
|
||||
* @return The optional result of the callback that has been executed with the given database.
|
||||
*/
|
||||
Mono<T> run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <em>Domain Type</em>.
|
||||
* Specifies reactive operations one can perform on a database, based on an <em>Domain
|
||||
* Type</em>.
|
||||
*
|
||||
* @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<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}.
|
||||
*/
|
||||
Mono<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 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<Long> count(Statement statement, Map<String, Object> 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<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 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<Long> count(String cypherQuery, Map<String, Object> parameters);
|
||||
|
||||
/**
|
||||
* Load all entities of a given type.
|
||||
*
|
||||
* @param domainType the type of the entities. Must not be {@code null}.
|
||||
* @param <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAll(Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAll(Statement statement, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Mono<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAll(String cypherQuery, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Mono<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> 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 <T> 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 <T> the type of the entities. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @return guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Flux<T> findAllById(Iterable<?> ids, Class<T> 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 <T> 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.
|
||||
*/
|
||||
<T> Mono<Boolean> existsById(Object id, Class<T> 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 <T> the type of the entity.
|
||||
* @return the saved instance.
|
||||
@@ -193,16 +184,18 @@ public interface ReactiveNeo4jOperations {
|
||||
<T> Mono<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).
|
||||
* <p>
|
||||
* 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 <T> 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 <T> 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 <T> the type of the entity.
|
||||
* @param <R> 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 <T> the type of the entity.
|
||||
* @return the saved instances.
|
||||
@@ -233,27 +227,31 @@ public interface ReactiveNeo4jOperations {
|
||||
<T> Flux<T> saveAll(Iterable<T> 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).
|
||||
* <p>
|
||||
* 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 <T> 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 <T> the type of the entity.
|
||||
* @return the saved instances.
|
||||
* @since 6.3
|
||||
*/
|
||||
default <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
|
||||
default <T> Flux<T> saveAllAs(Iterable<T> instances,
|
||||
BiPredicate<PropertyPath, Neo4jPersistentProperty> 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 <T> the type of the entity.
|
||||
* @param <R> 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 <T> the type of the entity.
|
||||
* @return a signal that the object has been deleted
|
||||
*/
|
||||
<T> Mono<Void> deleteById(Object id, Class<T> domainType);
|
||||
|
||||
<T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue);
|
||||
<T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> 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 <T> the type of the entity.
|
||||
* @return a signal that completes after all objects have been deleted
|
||||
*/
|
||||
<T> Mono<Void> deleteAllById(Iterable<?> ids, Class<T> 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<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 <T> 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 <T> the type of the objects returned by this query.
|
||||
* @return an executable query
|
||||
*/
|
||||
<T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> 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 <T> The type of the objects returned by this query.
|
||||
* @return An executable query
|
||||
* @param queryFragmentsAndParameters fragments and parameters to construct the query
|
||||
* from
|
||||
* @param <T> the type of the objects returned by this query.
|
||||
* @return an executable query
|
||||
*/
|
||||
<T> Mono<ExecutableQuery<T>> toExecutableQuery(Class<T> domainType,
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
|
||||
/**
|
||||
* An interface for controlling query execution in a reactive fashion.
|
||||
@@ -320,14 +322,19 @@ public interface ReactiveNeo4jOperations {
|
||||
interface ExecutableQuery<T> {
|
||||
|
||||
/**
|
||||
* @return All results returned by this query.
|
||||
* Returns all results returned by this query.
|
||||
* @return all results returned by this query
|
||||
*/
|
||||
Flux<T> 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<T> getSingleResult();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<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 ReactiveUserSelectionProvider getDefaultSelectionProvider() {
|
||||
|
||||
return DefaultReactiveUserSelectionProvider.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
enum DefaultReactiveUserSelectionProvider implements ReactiveUserSelectionProvider {
|
||||
INSTANCE;
|
||||
Mono<UserSelection> getUserSelection();
|
||||
|
||||
@Override
|
||||
public Mono<UserSelection> getUserSelection() {
|
||||
return Mono.just(UserSelection.connectedUser());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object, Object> newRelatedObjectsByType;
|
||||
|
||||
private Collection<Object> newRelatedObjects;
|
||||
|
||||
RelationshipHandler(Neo4jPersistentProperty property, @Nullable Object rawValue, Cardinality cardinality,
|
||||
Collection<Object> newRelatedObjects, Map<Object, Object> 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<Object> newRelationshipObjectCollection = Collections.emptyList();
|
||||
Map<Object, Object> 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<Object> newRelatedObjects;
|
||||
private final Map<Object, Object> newRelatedObjectsByType;
|
||||
|
||||
RelationshipHandler(Neo4jPersistentProperty property,
|
||||
@Nullable Object rawValue, Cardinality cardinality,
|
||||
Collection<Object> newRelatedObjects,
|
||||
Map<Object, Object> 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<Object> newCollection = (Collection<Object>) 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<Object> newCollection = (Collection<Object>) 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Notification> 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<String> 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<String> 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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> type of the domain class to map
|
||||
* @author Michael J. Simons
|
||||
* @since 6.0
|
||||
*/
|
||||
final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Record, T> {
|
||||
@@ -43,8 +45,7 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public T apply(TypeSystem typeSystem, Record record) {
|
||||
@Nullable public T apply(TypeSystem typeSystem, Record record) {
|
||||
|
||||
if (record.size() == 0) {
|
||||
throw new IllegalArgumentException("Record has no elements, cannot map nothing");
|
||||
@@ -57,11 +58,12 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
|
||||
return convertValue(record.get(0));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
T convertValue(Value source) {
|
||||
if (targetClass == Void.class || targetClass == void.class) {
|
||||
@Nullable T convertValue(Value source) {
|
||||
if (this.targetClass == Void.class || this.targetClass == void.class) {
|
||||
return null;
|
||||
}
|
||||
return source == null || source == Values.NULL ? null : conversionService.convert(source, targetClass);
|
||||
return (source == null || source == Values.NULL) ? null
|
||||
: this.conversionService.convert(source, this.targetClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.Entity;
|
||||
import org.neo4j.driver.types.MapAccessor;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.neo4j.core.mapping.Constants;
|
||||
@@ -71,36 +72,24 @@ import org.springframework.util.Assert;
|
||||
* Utilities for templates.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Metallica - Ride The Lightning
|
||||
* @since 6.0.9
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.0.9")
|
||||
public final class TemplateSupport {
|
||||
|
||||
/**
|
||||
* Indicator for an empty collection
|
||||
*/
|
||||
public static final class EmptyIterable {
|
||||
private EmptyIterable() {
|
||||
}
|
||||
private TemplateSupport() {
|
||||
}
|
||||
|
||||
enum FetchType {
|
||||
|
||||
ONE,
|
||||
ALL
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Class<?> findCommonElementType(Iterable<?> collection) {
|
||||
@Nullable public static Class<?> findCommonElementType(Iterable<?> collection) {
|
||||
|
||||
if (collection == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collection<Class<?>> 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<Class<?>> moveUp = c -> c != null && c != Object.class;
|
||||
Set<Class<?>> 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<Set<Class<?>>> 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<Class<?>> 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<PropertyFilter.ProjectedPath> 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<String, Object> mergeParameters(Statement statement, Map<String, Object> 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<String, Collection<String>> parameters = new HashMap<>(3);
|
||||
private final QueryFragments queryFragments;
|
||||
private final Function<Named, FunctionInvocation> elementIdFunction;
|
||||
|
||||
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments, Function<Named, FunctionInvocation> 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<Expression> 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 <T> 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 <T> the domain type
|
||||
* @return a mapping function
|
||||
*/
|
||||
static <T> Supplier<BiFunction<TypeSystem, MapAccessor, ?>> getAndDecorateMappingFunction(
|
||||
Neo4jMappingContext mappingContext, Class<T> domainType, @Nullable Class<?> resultType) {
|
||||
|
||||
Assert.notNull(mappingContext.getPersistentEntity(domainType), "Cannot get or create persistent entity");
|
||||
return () -> {
|
||||
BiFunction<TypeSystem, MapAccessor, ?> mappingFunction = mappingContext.getRequiredMappingFunctionFor(
|
||||
domainType);
|
||||
BiFunction<TypeSystem, MapAccessor, ?> 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 <T> 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 <T> the type of the entity
|
||||
* @return a new binder function that only works on the included properties.
|
||||
*/
|
||||
static <T> FilteredBinderFunction<T> createAndApplyPropertyFilter(
|
||||
Collection<PropertyFilter.ProjectedPath> includedProperties, Neo4jPersistentEntity<?> entityMetaData,
|
||||
Function<T, Map<String, Object>> binderFunction) {
|
||||
|
||||
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
|
||||
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties,
|
||||
entityMetaData);
|
||||
return new FilteredBinderFunction<>(includeProperty, binderFunction.andThen(tree -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> properties = (Map<String, Object>) 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 <T> 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 <T> the type of the domain type
|
||||
* @return a map as expected by the property filter.
|
||||
*/
|
||||
static <T> Collection<PropertyFilter.ProjectedPath> computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext,
|
||||
Class<T> domainType, BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
|
||||
static <T> Collection<PropertyFilter.ProjectedPath> computeIncludedPropertiesFromPredicate(
|
||||
Neo4jMappingContext mappingContext, Class<T> domainType,
|
||||
BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
|
||||
if (predicate == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Collection<PropertyFilter.ProjectedPath> 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 <T> Type of the entity
|
||||
*/
|
||||
static class FilteredBinderFunction<T> implements Function<T, Map<String, Object>> {
|
||||
final PropertyFilter filter;
|
||||
|
||||
final Function<T, Map<String, Object>> binderFunction;
|
||||
|
||||
FilteredBinderFunction(PropertyFilter filter, Function<T, Map<String, Object>> binderFunction) {
|
||||
this.filter = filter;
|
||||
this.binderFunction = binderFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> 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 <T> 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 <T> the type of the entity
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
static <T> void setGeneratedIdIfNecessary(
|
||||
Neo4jPersistentEntity<?> entityMetaData,
|
||||
PersistentPropertyAccessor<T> propertyAccessor,
|
||||
Object elementId,
|
||||
Optional<Entity> databaseEntity
|
||||
) {
|
||||
static <T> void setGeneratedIdIfNecessary(Neo4jPersistentEntity<?> entityMetaData,
|
||||
PersistentPropertyAccessor<T> propertyAccessor, Object elementId, Optional<Entity> 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 <T> 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 <T> the type of the entity
|
||||
* @return the actual related internal id being used.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
static <T> Object retrieveOrSetRelatedId(
|
||||
Neo4jPersistentEntity<?> entityMetadata,
|
||||
static <T> Object retrieveOrSetRelatedId(Neo4jPersistentEntity<?> entityMetadata,
|
||||
PersistentPropertyAccessor<T> propertyAccessor,
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<Entity> 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<String> ids) {
|
||||
@Nullable static Object convertToLongIdOrStringElementId(@Nullable Collection<String> 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<String, Collection<String>> parameters = new HashMap<>(3);
|
||||
|
||||
private final QueryFragments queryFragments;
|
||||
|
||||
private final Function<Named, FunctionInvocation> elementIdFunction;
|
||||
|
||||
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds,
|
||||
Collection<String> relatedNodeIds, QueryFragments queryFragments,
|
||||
Function<Named, FunctionInvocation> 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<Expression> 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 <T> the type of the entity
|
||||
*/
|
||||
static class FilteredBinderFunction<T> implements Function<T, Map<String, Object>> {
|
||||
|
||||
final PropertyFilter filter;
|
||||
|
||||
final Function<T, Map<String, Object>> binderFunction;
|
||||
|
||||
FilteredBinderFunction(PropertyFilter filter, Function<T, Map<String, Object>> binderFunction) {
|
||||
this.filter = filter;
|
||||
this.binderFunction = binderFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> apply(T t) {
|
||||
return this.binderFunction.apply(t);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object> 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<ConvertiblePair> convertibleTypes;
|
||||
|
||||
EnumConverter() {
|
||||
Set<ConvertiblePair> 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<ConvertiblePair> 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<Enum>) 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)}.
|
||||
* <p>
|
||||
* 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<ConvertiblePair> 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<ConvertiblePair> convertibleTypes;
|
||||
|
||||
EnumConverter() {
|
||||
Set<ConvertiblePair> 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<ConvertiblePair> 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<Enum>) 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)}.
|
||||
* <p>
|
||||
* 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<ConvertiblePair> 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>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).
|
||||
* <p>
|
||||
* 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).
|
||||
*
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>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()}.
|
||||
* <p>
|
||||
* 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<? extends Neo4jPersistentPropertyConverter<?>> 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<? extends Neo4jPersistentPropertyConverterFactory> 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<Object> {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* <a href="https://neo4j.com/docs/java-manual/current/cypher-workflow/#java-driver-type-mapping">Working with Cypher values</a>.
|
||||
* Conversions for all known Cypher types, directly supported by the driver. See <a href=
|
||||
* "https://neo4j.com/docs/java-manual/current/cypher-workflow/#java-driver-type-mapping">Working
|
||||
* with Cypher values</a>.
|
||||
*
|
||||
* @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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> 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 <T> the type to be returned
|
||||
* @return the converted value
|
||||
*/
|
||||
@Nullable
|
||||
<T> T convert(Object source, Class<T> targetType);
|
||||
@Nullable <T> T convert(Object source, Class<T> 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.
|
||||
*
|
||||
* <p>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
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Object> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> the type of the property to convert (the type of the actual attribute).
|
||||
* @author Michael J. Simons
|
||||
* @param <T> 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<T> {
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>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}.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>The calling mechanism will take care of adding and removing configured prefixes and transforming keys and values into
|
||||
* something that Neo4j can understand.
|
||||
* <p>
|
||||
* The calling mechanism will take care of adding and removing configured prefixes and
|
||||
* transforming keys and values into something that Neo4j can understand.
|
||||
*
|
||||
* @param <K> the type of the keys (Only Strings and Enums are supported).
|
||||
* @param <P> the type of the property.
|
||||
* @author Michael J. Simons
|
||||
* @param <K> The type of the keys (Only Strings and Enums are supported).
|
||||
* @param <P> 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<K, P> {
|
||||
|
||||
/**
|
||||
* 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<K, Value> 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<K, Value> source, Neo4jConversionService neo4jConversionService);
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* The Java driver itself has a good overview of the supported types:
|
||||
* <a href="https://neo4j.com/docs/driver-manual/1.7/cypher-values/#driver-neo4j-type-system">The Cypher type
|
||||
* system</a>.
|
||||
* The Java driver itself has a good overview of the supported types: <a href=
|
||||
* "https://neo4j.com/docs/driver-manual/1.7/cypher-values/#driver-neo4j-type-system">The
|
||||
* Cypher type system</a>.
|
||||
*
|
||||
* @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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<ConverterBuilder.ConverterAware> 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() {}
|
||||
}
|
||||
|
||||
@@ -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}. <br>
|
||||
* 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}. <br>
|
||||
* <br>
|
||||
* In cases a user has used Cypher and its <code>duration()</code> function, i.e. like so
|
||||
* <code>CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s</code> 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
|
||||
* <code>CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s</code>
|
||||
* 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. <br>
|
||||
* The Java Driver uses a <code>org.neo4j.driver.v1.types.IsoDuration</code>, embedded uses
|
||||
* <code>org.neo4j.values.storable.DurationValue</code> 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 <code>org.neo4j.driver.v1.types.IsoDuration</code>, embedded
|
||||
* uses <code>org.neo4j.values.storable.DurationValue</code> 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<TemporalAmount, TemporalAmount> {
|
||||
|
||||
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<TemporalAmount, TemporalUnit, Integer> TEMPORAL_UNIT_EXTRACTOR = (d, u) -> {
|
||||
@@ -59,6 +68,14 @@ final class TemporalAmountAdapter implements Function<TemporalAmount, TemporalAm
|
||||
return Math.toIntExact(d.get(u));
|
||||
};
|
||||
|
||||
private static boolean couldBePeriod(int type) {
|
||||
return (PERIOD_MASK & type) > 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<TemporalAmount, TemporalAm
|
||||
|
||||
if (couldBePeriod && !couldBeDuration) {
|
||||
return Period.of(values[FIELD_YEAR], values[FIELD_MONTH], values[FIELD_DAY]).normalized();
|
||||
} else if (couldBeDuration && !couldBePeriod) {
|
||||
}
|
||||
else if (couldBeDuration && !couldBePeriod) {
|
||||
return Duration.ofSeconds(values[FIELD_SECONDS]).plusNanos(values[FIELD_NANOS]);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return internalTemporalAmountRepresentation;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean couldBePeriod(int type) {
|
||||
return (PERIOD_MASK & type) > 0;
|
||||
}
|
||||
|
||||
private static boolean couldBeDuration(int type) {
|
||||
return (DURATION_MASK & type) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ConvertiblePair> convertibleTypes = Collections.unmodifiableSet(
|
||||
new HashSet<>(Arrays.asList(
|
||||
new ConvertiblePair(Value.class, TemporalAmount.class),
|
||||
new ConvertiblePair(TemporalAmount.class, Value.class)
|
||||
)));
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return convertibleTypes;
|
||||
private final Set<ConvertiblePair> 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<ConvertiblePair> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
/**
|
||||
* <!-- tag::intent[] -->
|
||||
Provides a set of simples types that SDN supports. The `Neo4jConversions` allows bringing in additional, custom
|
||||
converters.
|
||||
* <!-- end::intent[] -->
|
||||
* <!-- tag::intent[] --> Provides a set of simples types that SDN supports. The
|
||||
* `Neo4jConversions` allows bringing in additional, custom converters. <!-- end::intent[]
|
||||
* -->
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.data.neo4j.core.convert;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <strong>Warning</strong> Internal API, might change without further notice, even in patch releases.
|
||||
* <strong>Warning</strong> Internal API, might change without further notice, even in
|
||||
* patch releases.
|
||||
* <p>
|
||||
* 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<Neo4jPersistentEntity<?>, AssociationHandlerSupport> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
public static AssociationHandlerSupport of(Neo4jPersistentEntity<?> entity) {
|
||||
return CACHE.computeIfAbsent(entity, AssociationHandlerSupport::new);
|
||||
}
|
||||
private static final Map<Neo4jPersistentEntity<?>, 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<Neo4jPersistentProperty> handler) {
|
||||
entity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
|
||||
this.entity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
|
||||
if (!association.getInverse().isAnnotationPresent(TargetNode.class)) {
|
||||
handler.doWithAssociation(association);
|
||||
}
|
||||
});
|
||||
return entity;
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<NodeDescription<?>, 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<NodeDescription<?>, 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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Object> properties;
|
||||
|
||||
CreateRelationshipStatementHolder(Statement statement, Map<String, Object> properties) {
|
||||
@@ -45,11 +48,11 @@ public final class CreateRelationshipStatementHolder {
|
||||
}
|
||||
|
||||
public Statement getStatement() {
|
||||
return statement;
|
||||
return this.statement;
|
||||
}
|
||||
|
||||
public Map<String, Object> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<Class<?>> 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> T convert(Object source, Class<T> targetType) {
|
||||
return conversionService.convert(source, targetType);
|
||||
@Nullable public <T> T convert(Object source, Class<T> 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<Value, Class<?>, 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<Value, Class<?>, 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<Object> target = CollectionFactory
|
||||
.createCollection(rawType, Objects.requireNonNull(type.getComponentType()).getType(), value.size());
|
||||
Collection<Object> 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<Object, Value> 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<Object> hlp = (Neo4jPersistentPropertyConverter<Object>) 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<Object, Value> conversion, boolean applyConversionToCompleteCollection) {
|
||||
private Value writeValueImpl(@Nullable Object value, TypeInformation<?> type, Function<Object, Value> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
* <ul>
|
||||
* <li>when using internally generated (database) ids and the id property is {@literal null} or of a numeric primitive
|
||||
* less than or equal {@literal 0},</li>
|
||||
* <li>when using internally generated (database) ids and the id property is
|
||||
* {@literal null} or of a numeric primitive less than or equal {@literal 0},</li>
|
||||
* <li>when using externally generated values and the id is {@literal null},</li>
|
||||
* <li>when using assigned values without a version property or with a version property that is {@literal null}.</li>
|
||||
* <li>when using assigned values without a version property or with a version property
|
||||
* that is {@literal null}.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* An entity will not be treated as new
|
||||
* <ul>
|
||||
* <li>when using internally generated (database) ids and the id property has a non-null value greater than
|
||||
* {@literal 0},</li>
|
||||
* <li>when using externally generated values and the id property is not {@literal null},</li>
|
||||
* <li>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}.</li>
|
||||
* <li>when using internally generated (database) ids and the id property has a non-null
|
||||
* value greater than {@literal 0},</li>
|
||||
* <li>when using externally generated values and the id property is not
|
||||
* {@literal null},</li>
|
||||
* <li>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}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<Object, Object> valueLookup;
|
||||
|
||||
private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class<?> valueType,
|
||||
Function<Object, Object> 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<Object, Object> valueLookup;
|
||||
|
||||
private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class<?> valueType,
|
||||
Function<Object, Object> 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));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> 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<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
|
||||
implements Neo4jPersistentEntity<T> {
|
||||
|
||||
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Stream.concat(Stream.of(String.class), DEPRECATED_GENERATED_ID_TYPES.stream()).collect(Collectors.toUnmodifiableSet());
|
||||
private static final Set<Class<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
private final Lazy<Boolean> isRelationshipPropertiesEntity;
|
||||
|
||||
private final Lazy<Neo4jPersistentProperty> vectorProperty;
|
||||
|
||||
@Nullable
|
||||
private NodeDescription<?> parentNodeDescription;
|
||||
|
||||
private List<NodeDescription<?>> childNodeDescriptionsInHierarchy;
|
||||
|
||||
private final Lazy<Neo4jPersistentProperty> vectorProperty;
|
||||
|
||||
DefaultNeo4jPersistentEntity(TypeInformation<T> 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:<br>
|
||||
* 1. If there is no {@link Node} annotation, use the class name.<br>
|
||||
* 2. If there is an annotation but it has no properties set, use the class name.<br>
|
||||
* 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<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getUnderlyingClass()
|
||||
*/
|
||||
@Override
|
||||
public Class<T> 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<GraphPropertyDescription> getGraphProperties() {
|
||||
return this.graphProperties.get();
|
||||
@@ -173,10 +207,6 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
return this.additionalLabels.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getGraphProperty(String)
|
||||
*/
|
||||
@Override
|
||||
public Optional<GraphPropertyDescription> getGraphProperty(String fieldName) {
|
||||
return Optional.ofNullable(this.getPersistentProperty(fieldName));
|
||||
@@ -200,10 +230,6 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
return getRequiredAnnotation(RelationshipProperties.class).persistTypeInfo();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see BasicPersistentEntity#getFallbackIsNewStrategy()
|
||||
*/
|
||||
@Override
|
||||
protected IsNewStrategy getFallbackIsNewStrategy() {
|
||||
return DefaultNeo4jIsNewStrategy.basedOn(this);
|
||||
@@ -229,9 +255,9 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
|
||||
if (this.getIdDescription() == null
|
||||
&& (this.isAnnotationPresent(Node.class) || this.isAnnotationPresent(Persistent.class))) {
|
||||
&& (this.isAnnotationPresent(Node.class) || this.isAnnotationPresent(Persistent.class))) {
|
||||
|
||||
throw new IllegalStateException("Missing id property on " + this.getUnderlyingClass() + "");
|
||||
throw new IllegalStateException("Missing id property on " + this.getUnderlyingClass());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,34 +272,40 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
String propertyName = persistentProperty.getPropertyName();
|
||||
if (seen.contains(propertyName)) {
|
||||
duplicates.add(propertyName);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
seen.add(propertyName);
|
||||
}
|
||||
});
|
||||
|
||||
Assert.state(duplicates.isEmpty(), () -> 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<Class<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
Supplier<String> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
String propertyName = persistentProperty.getPropertyName();
|
||||
namesOfPropertiesWithDynamicLabels.add(propertyName);
|
||||
|
||||
Assert.state(persistentProperty.isCollectionLike(), () -> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
});
|
||||
|
||||
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()));
|
||||
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:<br>
|
||||
* 1. If there is no {@link Node} annotation, use the class name.<br>
|
||||
* 2. If there is an annotation but it has no properties set, use the class name.<br>
|
||||
* 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<String> 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:<br>
|
||||
* 1. If there is no {@link Node} annotation, empty {@code String} array.<br>
|
||||
* 2. If there is an annotation but it has no properties set, empty {@code String} array.<br>
|
||||
* 3a. If only {@link Node#labels()} property is set, use the all but the first one as the additional labels.<br>
|
||||
* 3b. If the {@link Node#primaryLabel()} property is set, use the all but the first one as the additional labels.<br>
|
||||
* 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.<br>
|
||||
* 3a. If only {@link Node#labels()} property is set, use the all but the first one as
|
||||
* the additional labels.<br>
|
||||
* 3b. If the {@link Node#primaryLabel()} property is set, use the all but the first
|
||||
* one as the additional labels.<br>
|
||||
* 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<String> computeOwnAdditionalLabels() {
|
||||
@@ -373,8 +388,10 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
if (!(nodeAnnotation == null || hasEmptyLabelInformation(nodeAnnotation))) {
|
||||
if (StringUtils.hasText(nodeAnnotation.primaryLabel())) {
|
||||
result.addAll(Arrays.asList(nodeAnnotation.labels()));
|
||||
} else {
|
||||
result.addAll(Arrays.asList(Arrays.copyOfRange(nodeAnnotation.labels(), 1, nodeAnnotation.labels().length)));
|
||||
}
|
||||
else {
|
||||
result.addAll(
|
||||
Arrays.asList(Arrays.copyOfRange(nodeAnnotation.labels(), 1, nodeAnnotation.labels().length)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +404,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
if (hasEmptyLabelInformation(nodeAnnotation)) {
|
||||
result.add(anInterface.getSimpleName());
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (StringUtils.hasText(nodeAnnotation.primaryLabel())) {
|
||||
result.add(nodeAnnotation.primaryLabel());
|
||||
}
|
||||
@@ -400,7 +418,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
private List<String> computeParentLabels() {
|
||||
List<String> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
parentLabels.add(parentNodeDescriptionCalculated.getPrimaryLabel());
|
||||
parentLabels.addAll(parentNodeDescriptionCalculated.getAdditionalLabels());
|
||||
}
|
||||
parentNodeDescriptionCalculated = (Neo4jPersistentEntity<?>) 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<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Neo4jPersistentProperty getVectorProperty() {
|
||||
return this.vectorProperty.getNullable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Neo4jPersistentProperty getRequiredVectorProperty() {
|
||||
Neo4jPersistentProperty property = getVectorProperty();
|
||||
if (property != null) {
|
||||
@@ -445,12 +457,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
throw new IllegalStateException(String.format("Required vector property not found for %s", this.getType()));
|
||||
}
|
||||
|
||||
private static boolean hasEmptyLabelInformation(Node nodeAnnotation) {
|
||||
return nodeAnnotation.labels().length < 1 && !StringUtils.hasText(nodeAnnotation.primaryLabel());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private IdDescription computeIdDescription() {
|
||||
@Nullable private IdDescription computeIdDescription() {
|
||||
|
||||
Neo4jPersistentProperty idProperty = this.getIdProperty();
|
||||
if (idProperty == null) {
|
||||
@@ -477,8 +484,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
// Internally generated ids.
|
||||
if (idGeneratorClass == GeneratedValue.InternalIdGenerator.class && idGeneratorRef.isEmpty()) {
|
||||
if (idProperty.findAnnotation(Property.class) != null) {
|
||||
throw new IllegalArgumentException("Cannot use internal id strategy with custom property " + propertyName
|
||||
+ " on entity class " + this.getUnderlyingClass().getName());
|
||||
throw new IllegalArgumentException("Cannot use internal id strategy with custom property "
|
||||
+ propertyName + " on entity class " + this.getUnderlyingClass().getName());
|
||||
}
|
||||
|
||||
if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) {
|
||||
@@ -499,24 +506,30 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
}
|
||||
|
||||
// Externally generated ids.
|
||||
return IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this), idGeneratorClass, idGeneratorRef, propertyName);
|
||||
return IdDescription.forExternallyGeneratedIds(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this), idGeneratorClass,
|
||||
idGeneratorRef, propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RelationshipDescription> getRelationships() {
|
||||
|
||||
final List<RelationshipDescription> relationships = new ArrayList<>();
|
||||
AssociationHandlerSupport.of(this).doWithAssociations(
|
||||
(Association<Neo4jPersistentProperty> association) -> relationships.add((RelationshipDescription) association));
|
||||
AssociationHandlerSupport.of(this)
|
||||
.doWithAssociations((Association<Neo4jPersistentProperty> association) -> relationships
|
||||
.add((RelationshipDescription) association));
|
||||
return Collections.unmodifiableCollection(relationships);
|
||||
}
|
||||
|
||||
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter) {
|
||||
@Override
|
||||
public Collection<RelationshipDescription> getRelationshipsInHierarchy(
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter) {
|
||||
|
||||
return getRelationshipsInHierarchy(propertyFilter, PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass()));
|
||||
return getRelationshipsInHierarchy(propertyFilter,
|
||||
PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass()));
|
||||
}
|
||||
|
||||
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter, PropertyFilter.RelaxedPropertyPath path) {
|
||||
Collection<RelationshipDescription> getRelationshipsInHierarchy(
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter, PropertyFilter.RelaxedPropertyPath path) {
|
||||
|
||||
Collection<RelationshipDescription> relationships = new HashSet<>(getRelationships());
|
||||
for (NodeDescription<?> childDescription : getChildNodeDescriptionsInHierarchy()) {
|
||||
@@ -525,18 +538,21 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
String fieldName = concreteRelationship.getFieldName();
|
||||
NodeDescription<?> 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.RelaxedPropertyPath> propertyFilter, RelationshipDescription relationshipDescription, PropertyFilter.RelaxedPropertyPath path) {
|
||||
private boolean filterProperties(Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter,
|
||||
RelationshipDescription relationshipDescription, PropertyFilter.RelaxedPropertyPath path) {
|
||||
PropertyFilter.RelaxedPropertyPath from = path.append(relationshipDescription.getFieldName());
|
||||
return propertyFilter.test(from);
|
||||
}
|
||||
@@ -580,14 +596,15 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
@Override
|
||||
public List<NodeDescription<?>> getChildNodeDescriptionsInHierarchy() {
|
||||
return childNodeDescriptionsInHierarchy;
|
||||
return this.childNodeDescriptionsInHierarchy;
|
||||
}
|
||||
|
||||
private List<NodeDescription<?>> computeChildNodeDescriptionInHierarchy() {
|
||||
List<NodeDescription<?>> childNodes = new ArrayList<>(childNodeDescriptions);
|
||||
List<NodeDescription<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
return childNodes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public NodeDescription<?> 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<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
return calculatePossibleCircles(includeField);
|
||||
@@ -616,11 +634,13 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
Set<NodeDescription<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
Set<NodeDescription<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean calculatePossibleCircles(NodeDescription<?> nodeDescription, Set<NodeDescription<?>> visitedNodes, Predicate<PropertyFilter.RelaxedPropertyPath> includeField, PropertyFilter.RelaxedPropertyPath path) {
|
||||
Collection<RelationshipDescription> allRelationships = new HashSet<>(((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includeField, path));
|
||||
private boolean calculatePossibleCircles(NodeDescription<?> nodeDescription, Set<NodeDescription<?>> visitedNodes,
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> includeField, PropertyFilter.RelaxedPropertyPath path) {
|
||||
Collection<RelationshipDescription> allRelationships = new HashSet<>(
|
||||
((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includeField, path));
|
||||
|
||||
Collection<NodeDescription<?>> visitedTargetNodes = new HashSet<>();
|
||||
for (RelationshipDescription relationship : allRelationships) {
|
||||
@@ -662,17 +691,24 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
Set<NodeDescription<?>> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultNeo4jPersistentEntity{" +
|
||||
"primaryLabel='" + primaryLabel + '\'' +
|
||||
'}';
|
||||
return "DefaultNeo4jPersistentEntity{" + "primaryLabel='" + this.primaryLabel + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Optional;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.data.annotation.ReadOnlyProperty;
|
||||
import org.springframework.data.mapping.Association;
|
||||
@@ -44,6 +45,8 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of the {@link Neo4jPersistentProperty}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 6.0
|
||||
*/
|
||||
@@ -51,10 +54,13 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
|
||||
implements Neo4jPersistentProperty {
|
||||
|
||||
private final Lazy<String> 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<Boolean> 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<?, Neo4jPersistentProperty> 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<RelationshipDescription> obverseRelationshipDescription = obverseOwner.getRelationships().stream()
|
||||
.filter(rel -> rel.getType().equals(type)
|
||||
&& rel.getTarget().equals(this.getOwner())
|
||||
&& rel.getDirection() == direction.opposite()).findFirst();
|
||||
Optional<RelationshipDescription> 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<org.springframework.data.neo4j.core.schema.Property> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<EntityInstanceWithSource, Object> {
|
||||
@@ -53,11 +53,11 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
|
||||
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DtoInstantiatingConverter.class));
|
||||
|
||||
private final Class<?> 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<EntityInstance
|
||||
}
|
||||
|
||||
public Object convertDirectly(Object entityInstance) {
|
||||
Neo4jPersistentEntity<?> sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass());
|
||||
Neo4jPersistentEntity<?> sourceEntity = this.context.getRequiredPersistentEntity(entityInstance.getClass());
|
||||
PersistentPropertyAccessor<Object> 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<Object> dtoAccessor = targetEntity.getPropertyAccessor(dto);
|
||||
PropertyHandlerSupport.of(targetEntity).doWithProperties(property -> {
|
||||
@@ -98,9 +96,8 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object getPropertyValueDirectlyFor(PersistentProperty<?> 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<EntityInstance
|
||||
}
|
||||
|
||||
Object result = sourceAccessor.getProperty(sourceProperty);
|
||||
if (result != null && targetProperty.isEntity() && !targetProperty.getTypeInformation().isAssignableFrom(sourceProperty.getTypeInformation())) {
|
||||
if (result != null && targetProperty.isEntity()
|
||||
&& !targetProperty.getTypeInformation().isAssignableFrom(sourceProperty.getTypeInformation())) {
|
||||
return new DtoInstantiatingConverter(targetProperty.getType(), this.context).convertDirectly(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object convert(EntityInstanceWithSource entityInstanceAndSource) {
|
||||
@Nullable public Object convert(EntityInstanceWithSource entityInstanceAndSource) {
|
||||
|
||||
Object entityInstance = entityInstanceAndSource.getEntityInstance();
|
||||
if (targetType.isInterface() || targetType.isInstance(entityInstance)) {
|
||||
if (this.targetType.isInterface() || this.targetType.isInstance(entityInstance)) {
|
||||
return entityInstance;
|
||||
}
|
||||
|
||||
Neo4jPersistentEntity<?> sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass());
|
||||
Neo4jPersistentEntity<?> sourceEntity = this.context.getRequiredPersistentEntity(entityInstance.getClass());
|
||||
PersistentPropertyAccessor<Object> 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<Object> dtoAccessor = targetEntity.getPropertyAccessor(dto);
|
||||
targetEntity.doWithAll(property -> setPropertyOnDtoObject(entityInstanceAndSource, sourceEntity, sourceAccessor,
|
||||
@@ -148,11 +144,11 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
|
||||
}
|
||||
|
||||
private ParameterValueProvider<Neo4jPersistentProperty> getParameterValueProvider(
|
||||
Neo4jPersistentEntity<?> targetEntity,
|
||||
Function<Neo4jPersistentProperty, Object> extractFromSource
|
||||
) {
|
||||
Neo4jPersistentEntity<?> targetEntity, Function<Neo4jPersistentProperty, Object> 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> T getParameterValue(Parameter<T, Neo4jPersistentProperty> parameter) {
|
||||
String parameterName = parameter.getName();
|
||||
@@ -163,7 +159,7 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
|
||||
Neo4jPersistentProperty targetProperty = targetEntity.getPersistentProperty(parameterName);
|
||||
if (targetProperty == null) {
|
||||
throw new MappingException("Cannot map constructor parameter " + parameterName
|
||||
+ " to a property of class " + targetType);
|
||||
+ " to a property of class " + DtoInstantiatingConverter.this.targetType);
|
||||
}
|
||||
return (T) extractFromSource.apply(targetProperty);
|
||||
}
|
||||
@@ -183,8 +179,7 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
|
||||
dtoAccessor.setProperty(property, propertyValue);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object getPropertyValueFor(Neo4jPersistentProperty targetProperty, PersistentEntity<?, ?> 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<EntityInstance
|
||||
}
|
||||
|
||||
if (!sourceRecord.containsKey(targetPropertyName)) {
|
||||
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, 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<EntityInstance
|
||||
log.warn(() -> 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<Value, Object> 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<Object> returnedValues = property.asList(singleValue);
|
||||
Collection<Object> target = CollectionFactory
|
||||
.createCollection(targetProperty.getType(), actualType, returnedValues.size());
|
||||
Collection<Object> 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<EntityInstance
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user