Merge SDN/RX into SDN.
This commit is contained in:
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,17 +1,33 @@
|
||||
.idea/
|
||||
*/target/
|
||||
*.iml
|
||||
spring-data-neo4j-examples/sdn-boot/bower_components/
|
||||
.buildpath
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
*/neo4j-home/
|
||||
target/
|
||||
.idea/
|
||||
*.class
|
||||
*.iml
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
*.db
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
/build/
|
||||
|
||||
### Visual Studio Code ###
|
||||
.vscode
|
||||
|
||||
### Misc ###
|
||||
.DS_Store
|
||||
.classpath
|
||||
.flattened-pom.xml
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
110
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Executable file
110
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Executable file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
import java.net.*;
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Properties;
|
||||
|
||||
public class MavenWrapperDownloader {
|
||||
|
||||
/**
|
||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||
*/
|
||||
private static final String DEFAULT_DOWNLOAD_URL =
|
||||
"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
|
||||
|
||||
/**
|
||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||
* use instead of the default one.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||
".mvn/wrapper/maven-wrapper.properties";
|
||||
|
||||
/**
|
||||
* Path where the maven-wrapper.jar will be saved to.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||
".mvn/wrapper/maven-wrapper.jar";
|
||||
|
||||
/**
|
||||
* Name of the property which should be used to override the default download url for the wrapper.
|
||||
*/
|
||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.out.println("- Downloader started");
|
||||
File baseDirectory = new File(args[0]);
|
||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||
|
||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||
// wrapperUrl parameter.
|
||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||
String url = DEFAULT_DOWNLOAD_URL;
|
||||
if(mavenWrapperPropertyFile.exists()) {
|
||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||
try {
|
||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||
Properties mavenWrapperProperties = new Properties();
|
||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||
} catch (IOException e) {
|
||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||
} finally {
|
||||
try {
|
||||
if(mavenWrapperPropertyFileInputStream != null) {
|
||||
mavenWrapperPropertyFileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore ...
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading from: : " + url);
|
||||
|
||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||
if(!outputFile.getParentFile().exists()) {
|
||||
if(!outputFile.getParentFile().mkdirs()) {
|
||||
System.out.println(
|
||||
"- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||
try {
|
||||
downloadFileFromURL(url, outputFile);
|
||||
System.out.println("Done");
|
||||
System.exit(0);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("- Error downloading");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||
URL website = new URL(urlString);
|
||||
ReadableByteChannel rbc;
|
||||
rbc = Channels.newChannel(website.openStream());
|
||||
FileOutputStream fos = new FileOutputStream(destination);
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
fos.close();
|
||||
rbc.close();
|
||||
}
|
||||
|
||||
}
|
||||
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1 +1 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.1/apache-maven-3.6.1-bin.zip
|
||||
234
README.adoc
234
README.adoc
@@ -1,203 +1,147 @@
|
||||
image:https://spring.io/badges/spring-data-neo4j/ga.svg[title=Spring Data Neo4j,link=https://projects.spring.io/spring-data-neo4j#quick-start]
|
||||
image:https://spring.io/badges/spring-data-neo4j/snapshot.svg[title=Spring Data Neo4j,link=https://projects.spring.io/spring-data-neo4j#quick-start]
|
||||
= Spring Data Neo4j⚡️RX
|
||||
:sectanchors:
|
||||
|
||||
= Spring Data Neo4j
|
||||
// tag::properties[]
|
||||
:groupId: org.neo4j.springframework.data
|
||||
:artifactId: spring-data-neo4j-rx
|
||||
:artifactIdStarter: spring-data-neo4j-rx-spring-boot-starter
|
||||
|
||||
:neo4j-version: 4.0.4
|
||||
:spring-boot-version: 2.3.0.RELEASE
|
||||
:spring-data-neo4j-rx-version: 1.1.1
|
||||
// end::properties[]
|
||||
|
||||
image:https://img.shields.io/maven-central/v/org.neo4j.springframework.data/spring-data-neo4j-rx.svg[Maven Central,link=http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.neo4j.springframework.data%22%20AND%20a%3A%22spring-data-neo4j-rx%22]
|
||||
|
||||
[abstract]
|
||||
--
|
||||
Spring Data Neo4j⚡️RX - or in short _SDN/RX_ - is an ongoing effort to create the next generation of Spring Data Neo4j, with full reactive support and lightweight mapping.
|
||||
SDN/RX will work with immutable entities, regardless whether written in Java or Kotlin.
|
||||
--
|
||||
|
||||
The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services.
|
||||
|
||||
The Spring Data Neo4j project aims to provide a familiar and consistent Spring-based programming model for integrating with the https://neo4j.com/[Neo4j] Graph Database.
|
||||
The SDN/RX project aims to provide a familiar and consistent Spring-based programming model for integrating with the https://neo4j.com/[Neo4j] Graph Database.
|
||||
|
||||
== Code of Conduct
|
||||
== Manual
|
||||
|
||||
This project is governed by the link:CODE_OF_CONDUCT.adoc[Spring Code of Conduct]. By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
For a gentle introduction and some getting started guides, please use our
|
||||
https://neo4j.github.io/sdn-rx[Manual].
|
||||
|
||||
== Getting Started
|
||||
|
||||
Here is a quick teaser of an application using Spring Data Repositories in Java:
|
||||
Here is a quick teaser of a reactive application using Spring Data Repositories in Java:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@NodeEntity
|
||||
@Node
|
||||
public class Person {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
@Relationship(type = "FRIEND", direction = "OUTGOING")
|
||||
private Set<Person> friends;
|
||||
|
||||
public Person() {}
|
||||
public Person(String name) { this.name = name; }
|
||||
|
||||
private void knows(Person friend) { friends.add(friend); }
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@Repository
|
||||
public interface PersonRepository extends Neo4jRepository<Person, Long> {
|
||||
interface PersonRepository extends ReactiveNeo4jRepository<Person, Long> {
|
||||
|
||||
List<Person> findByName(String name);
|
||||
Flux<Person> findAllByName(String name);
|
||||
|
||||
List<Person> findByNameLike(String name);
|
||||
Flux<Person> findAllByNameLike(String name);
|
||||
}
|
||||
|
||||
@Service
|
||||
public class MyService {
|
||||
class MyService {
|
||||
|
||||
@Autowired
|
||||
private final PersonRepository repository;
|
||||
|
||||
@Transactional
|
||||
public void doWork() {
|
||||
public Flux<Person> doWork() {
|
||||
|
||||
Person jon = new Person("Jon");
|
||||
Person emil = new Person("Emil");
|
||||
Person rod = new Person("Rod");
|
||||
|
||||
emil.knows(jon);
|
||||
emil.knows(rod);
|
||||
Person gerrit = new Person("Gerrit");
|
||||
Person michael = new Person("Michael");
|
||||
|
||||
// Persist entities and relationships to graph database
|
||||
personRepository.save(emil);
|
||||
|
||||
for (Person friend : emil.getFriends()) {
|
||||
System.out.println("Friend: " + friend);
|
||||
}
|
||||
|
||||
// Control loading depth
|
||||
Person thatSamejon = personRepository.findOne(id, 2);
|
||||
for (Person friend : jon.getFriends()) {
|
||||
System.out.println("Jon's friends to depth 2: " + friend);
|
||||
}
|
||||
return repository.saveAll(Flux.just(emil, gerrit, michael));
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableNeo4jRepositories
|
||||
@EnableTransactionManagement
|
||||
public class MyConfiguration {
|
||||
|
||||
@Bean
|
||||
public SessionFactory sessionFactory(org.neo4j.ogm.config.Configuration configuration) {
|
||||
// with domain entity base package(s)
|
||||
return new SessionFactory(configuration, "com.example.person.domain");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public org.neo4j.ogm.config.Configuration configuration() {
|
||||
org.neo4j.ogm.config.Configuration configuration = new org.neo4j.ogm.config.Configuration.Builder()
|
||||
.uri("bolt://localhost")
|
||||
.credentials("user", "secret")
|
||||
.build();
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Neo4jTransactionManager transactionManager() {
|
||||
return new Neo4jTransactionManager(sessionFactory());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
TIP: SDN/RX is not only about reactive support, all features are available in both ways: Imperative and reactive, we
|
||||
only prefer to showcase the new reactive database access support here.
|
||||
|
||||
=== Maven configuration
|
||||
|
||||
Add the Maven dependency:
|
||||
==== With Spring Boot
|
||||
|
||||
[source,xml]
|
||||
If you are on https://spring.io/projects/spring-boot[Spring Boot], all you have to do is to add our starter:
|
||||
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
<version>${version}.RELEASE</version>
|
||||
<groupId>{groupId}</groupId>
|
||||
<artifactId>{artifactIdStarter}</artifactId>
|
||||
<version>{spring-data-neo4j-rx-version}</version>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version.
|
||||
and configure your database connection:
|
||||
|
||||
[source,xml]
|
||||
[source,properties]
|
||||
----
|
||||
org.neo4j.driver.uri=bolt://localhost:7687
|
||||
org.neo4j.driver.authentication.username=neo4j
|
||||
org.neo4j.driver.authentication.password=secret
|
||||
----
|
||||
|
||||
Please have a look at our https://neo4j.github.io/sdn-rx[manual] for an overview about the architecture, how to define
|
||||
mappings and more.
|
||||
|
||||
==== Without Spring Boot
|
||||
|
||||
If you are using a plain Spring Framework project without Spring Boot, please add this Maven dependency:
|
||||
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
<version>${version}.BUILD-SNAPSHOT</version>
|
||||
<groupId>{groupId}</groupId>
|
||||
<artifactId>{artifactId}</artifactId>
|
||||
<version>{spring-data-neo4j-rx-version}</version>
|
||||
</dependency>
|
||||
|
||||
<repository>
|
||||
<id>spring-libs-snapshot</id>
|
||||
<name>Spring Snapshot Repository</name>
|
||||
<url>https://repo.spring.io/libs-snapshot</url>
|
||||
</repository>
|
||||
----
|
||||
|
||||
Please find the setup for Gradle based projects in the the https://docs.spring.io/spring-data/data-neo4j/docs/current/reference/html/[Reference Manual].
|
||||
and configure SDN/RX for reactive database access like this:
|
||||
|
||||
Spring Data Neo4j provides support for connecting to all of Neo4j's java drivers:
|
||||
|
||||
* Bolt
|
||||
* HTTP
|
||||
* Embedded
|
||||
|
||||
Depending on your need, you'll have to add one additional Neo4j-OGM module.
|
||||
Please refer to the reference linked above.
|
||||
|
||||
== Getting Help
|
||||
|
||||
Having trouble with Spring Data? We’d love to help!
|
||||
|
||||
* Check the
|
||||
https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/[reference documentation], and https://docs.spring.io/spring-data/neo4j/docs/current/api/[Javadocs].
|
||||
* Learn the Spring basics – Spring Data builds on Spring Framework, check the https://spring.io[spring.io] web-site for a wealth of reference documentation.
|
||||
If you are just starting out with Spring, try one of the https://spring.io/guides[guides].
|
||||
* If you are upgrading, check out the https://docs.spring.io/spring-data/neo4j/docs/current/changelog.txt[changelog] for "`new and noteworthy`" features.
|
||||
* Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/questions/tagged/spring-data-neo4j-5[spring-data-neo4j-5].
|
||||
* Report bugs with Spring Data Neo4j at https://jira.spring.io/browse/DATAGRAPH[jira.spring.io/browse/DATAGRAPH].
|
||||
|
||||
== Reporting Issues
|
||||
|
||||
Spring Data uses JIRA as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below:
|
||||
|
||||
* Before you log a bug, please search the
|
||||
https://jira.spring.io/browse/DATAGRAPH[issue tracker] to see if someone has already reported the problem.
|
||||
* If the issue doesn’t already exist, https://jira.spring.io/browse/DATAGRAPH[create a new issue].
|
||||
* Please provide as much information as possible with the issue report, we like to know the version of Spring Data that you are using and JVM version.
|
||||
* If you need to paste code, or include a stack trace use JIRA `{code}…{code}` escapes before and after your text.
|
||||
* If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code.
|
||||
|
||||
== Building from Source
|
||||
|
||||
You don’t need to build from source to use Spring Data (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Data can be easily built with the https://github.com/takari/maven-wrapper[maven wrapper].
|
||||
You also need JDK 1.8.
|
||||
|
||||
[source,bash]
|
||||
[source,java]
|
||||
----
|
||||
$ ./mvnw clean install
|
||||
@Configuration
|
||||
@EnableReactiveNeo4jRepositories
|
||||
@EnableTransactionManagement
|
||||
class MyConfiguration extends AbstractReactiveNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<String> getMappingBasePackages() {
|
||||
return Collections.singletonList(Person.class.getPackage().getName());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.5.0 or above].
|
||||
The imperative version looks pretty much the same but uses `EnableNeo4jRepositories` and `AbstractNeo4jConfig`.
|
||||
|
||||
_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please sign the https://cla.pivotal.io/sign/spring[Contributor's Agreement] before your first non-trivial change._
|
||||
IMPORTANT: We recommend Spring Boot, the automatic configuration and especially the dependency management
|
||||
through the Starters in contrast to the manual work of managing dependencies and configuration.
|
||||
+
|
||||
Please consult our https://neo4j.github.io/sdn-rx[manual] for more information.
|
||||
|
||||
=== Building reference documentation
|
||||
=== Building SDN/RX
|
||||
|
||||
Building the documentation builds also the project without running tests.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./mvnw clean install -Pdistribute
|
||||
----
|
||||
|
||||
The generated documentation is available from `target/site/reference/html/index.html`.
|
||||
|
||||
== Guides
|
||||
|
||||
The https://spring.io/[spring.io] site contains several guides that show how to use Spring Data step-by-step:
|
||||
|
||||
* https://spring.io/guides/gs/accessing-data-neo4j/[Accessing Data with Neo4j] is a very basic guide that shows you how to create a simple application and how to access data using repositories.
|
||||
* https://spring.io/guides/gs/accessing-neo4j-data-rest/[Accessing Neo4j Data with REST] is a guide to creating a REST web service exposing data stored in Neo4j through repositories.
|
||||
|
||||
== Examples
|
||||
|
||||
* https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail.
|
||||
|
||||
== License
|
||||
|
||||
Spring Data Neo4j is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].
|
||||
Please have a look at the documentation: https://neo4j.github.io/sdn-rx/current/#building-sdn-rx[Building SDN/RX].
|
||||
|
||||
26
etc/adr/adr-001.adoc
Normal file
26
etc/adr/adr-001.adoc
Normal file
@@ -0,0 +1,26 @@
|
||||
== ADR 1: Configuration of Id-Mapping
|
||||
|
||||
=== Status
|
||||
|
||||
accepted
|
||||
|
||||
=== Context
|
||||
|
||||
SDN RX needs to provide a configuration of Ids mappings.
|
||||
Ids can either be internal (native Neo4j) Ids or generated Ids.
|
||||
|
||||
=== Decision
|
||||
|
||||
The configuration should not be ambiguous.
|
||||
`@org.springframework.data.annotation.Id` will be used as the marker for an Id.
|
||||
A _strategy_ will be used to decide whether the annotated attribute will be mapped to `id(node)` or set from the external.
|
||||
The strategy will either be `internal`, `assigned` or `generated`.
|
||||
`generated` will require an additional attribute of `generator`.
|
||||
|
||||
The internal strategy will be the default.
|
||||
|
||||
To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.neo4j.springframework.data.core.schema.Id`
|
||||
|
||||
=== Consequences
|
||||
|
||||
We are still compatible with the OGM 3.1+ approach of recommending `@Id long id;` while providing a clear direction for the user.
|
||||
21
etc/adr/adr-002.adoc
Normal file
21
etc/adr/adr-002.adoc
Normal file
@@ -0,0 +1,21 @@
|
||||
== ADR 2: Build on and compile for JDK11
|
||||
|
||||
=== Status
|
||||
|
||||
proposed
|
||||
|
||||
=== Context
|
||||
|
||||
JDK 8 is deprecated and has left support.
|
||||
Neo4j 4.0 is already build on JDK 11 and probably compiled to it as well.
|
||||
|
||||
=== Decision
|
||||
|
||||
Not yet made.
|
||||
|
||||
=== Consequences
|
||||
|
||||
* + We would target a modern platform
|
||||
* + We would benefit from SDK enhancements
|
||||
* 0 We could benefit from JPMS
|
||||
* - Adoption rate could be lower.
|
||||
20
etc/adr/adr-003.adoc
Normal file
20
etc/adr/adr-003.adoc
Normal file
@@ -0,0 +1,20 @@
|
||||
== ADR 3: Public classes that are part of internal API only must be final
|
||||
|
||||
=== Status
|
||||
|
||||
accepted
|
||||
|
||||
=== Context
|
||||
|
||||
Due to the fact that we are not yet on the module path, we need to have some classes public defined that are not meant
|
||||
to be part of the public API.
|
||||
|
||||
=== Decision
|
||||
|
||||
Those classes should be marked `@API(status = API.Status.INTERNAL, since = "1.0")` as well as made final to at least
|
||||
prevent people from inheriting from them.
|
||||
|
||||
=== Consequences
|
||||
|
||||
Potentially problems with some Spring proxies.
|
||||
Need to be solved on a case by case incident.
|
||||
22
etc/adr/adr-004.adoc
Normal file
22
etc/adr/adr-004.adoc
Normal file
@@ -0,0 +1,22 @@
|
||||
== ADR 4: Drop the notion of the `NodeManager`
|
||||
|
||||
=== Status
|
||||
|
||||
accepted
|
||||
|
||||
=== Context
|
||||
|
||||
We introduced the `NodeManager` as a pendan to Hibernates `EntityManager` and with it, a concept of a persistence context, tracking changes.
|
||||
This setup is required for updating only changed properties and also having implicit saves.
|
||||
|
||||
=== Decision
|
||||
|
||||
The previous versions of SDN and OGM all copied the concept of having a tracking of entities.
|
||||
We decided against it this time to remove complexity.
|
||||
We will update all properties each time a node is save, relying on the database to do this in an efficient way.
|
||||
|
||||
Relationships will be updated via smart queries.
|
||||
|
||||
=== Consequences
|
||||
|
||||
The biggest impact will probably more network traffic with models having a huge number of properties on a single domain object.
|
||||
342
etc/adr/general-discussion.adoc
Normal file
342
etc/adr/general-discussion.adoc
Normal file
@@ -0,0 +1,342 @@
|
||||
= General architectural discussions about Spring Data Neo4j⚡️RX
|
||||
|
||||
[abstract]
|
||||
--
|
||||
This is a work in progress project determining a possible future form of Spring Data Neo4j.
|
||||
Expect the README and even more the project to change quite a lot in the future weeks.
|
||||
--
|
||||
|
||||
== Architectural guidelines and principles
|
||||
|
||||
The next version of Spring Data Neo4j should be designed with the following principles in mind:
|
||||
|
||||
* Rely completely on the https://github.com/neo4j/neo4j-java-driver[Neo4j Java Driver], without introducing another "driver" or "transport" layer between the mapping framework and the driver.
|
||||
* Immutable entities and thus full support for Kotlin's data classes right from the start.
|
||||
* Work result item / record and not result set oriented, thus not reading the complete result set before the mapping starts, but make a "row" the foundation for any mapping.
|
||||
This encourages generation of optimized queries, which should greatly reduce the object graph impedance mismatch we see in some projects using Neo4j-OGM.
|
||||
* Follow up on the reactive story for database access in general. Being immutable and row oriented are two main requirements for making things possible.
|
||||
|
||||
=== Modules
|
||||
|
||||
So far we have identified the following modules:
|
||||
|
||||
* Schema: Should read a set of given classes and create a schema (Metagraph) from it
|
||||
* Mapping: Should take care of hydrating from results to domain objects and dehydrating vice versa. It can depend on schema, but only as a provider for property matching
|
||||
* Lifecycle: Lifecycle must not depend directly on mapping, but should only care whether an Object and its Relations are managed or not
|
||||
* Querying: Generates cypher queries, depends on schema
|
||||
|
||||
Those will be reassembled as packages inside Spring Data Neo4j RX.
|
||||
There are no short-term planes to create additional artifacts from those.
|
||||
|
||||
[[schema]]
|
||||
==== Schema
|
||||
|
||||
We used the new Spring Data JDBC project as blueprint for some ideas now.
|
||||
Spring Data JDBC doesn't build up the schema upfront.
|
||||
Each time, a persistent entity is requested from the mapping context, that entity is read and fully described, including properties and all associations.
|
||||
We can implement it in the same way.
|
||||
The mapping context would return instances of `Neo4jPersistentEntity` which implements a Spring Data interface.
|
||||
To fulfill the contract however, we would read the classes and store them in a schema that is free of Spring dependencies.
|
||||
That way we we can avoid a compile time dependency to Spring Data and have an independent schema module, in which the mapping context is the connecting adapter.
|
||||
|
||||
Spring Data JDBC doesn't restrict the supported or scanned classes from Spring Data sides.
|
||||
Our schema should also support non-annotated classes and be smart about naming things, but we will require at least the `@Node` or `@Relationship` annotation to the outside world.
|
||||
|
||||
The schema will life independent from Spring classes in `org.neo4j.springframework.data.core.schema`.
|
||||
Each property of a class that is not identified as a simple type by `org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well.
|
||||
|
||||
==== Context
|
||||
|
||||
NOTE: Context in this sections refers especially to dirty tracking and dealing with state of entities.
|
||||
|
||||
We decided against a context for tracking changes, much like Spring Data JDBC did.
|
||||
|
||||
=== Other principles
|
||||
|
||||
* Ensure that the underlying store (Neo4j) leaks as little as possible into the mapping.
|
||||
I.e. reuse `@Id` etc. and avoid adding custom variants of annotation whenever possible.
|
||||
|
||||
=== The embedded "problem"
|
||||
|
||||
Supporting the embedded use case will be solved on the drivers level.
|
||||
|
||||
=== Relationships
|
||||
|
||||
=== Simple relationships
|
||||
|
||||
We provide `@Relationship` for mapping relationships without properties.
|
||||
This annotation shall be used for 1:1 and 1:n mappings.
|
||||
It provides an attribute to specifiy the name of the relationship.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Node("User")
|
||||
static class UserNode {
|
||||
|
||||
@Relationship(type = "OWNS")
|
||||
List<BikeNode> bikes;
|
||||
}
|
||||
|
||||
static class BikeNode {
|
||||
|
||||
UserNode owner;
|
||||
|
||||
UserNode renter;
|
||||
}
|
||||
----
|
||||
|
||||
=== "Rich" relationships
|
||||
|
||||
There should be no means of using a relationship as aggregate root in SDN/RX (like it is today the case with `@RelationshipEntity`).
|
||||
Instead we suggest that properties of relationships are mapped to POJOs.
|
||||
This has the following requirements:
|
||||
On the node representing the start node, the relationship (either 1:1 or 1:n) has to be annotated with `@Relationship` specifying the type of the end node like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Relationship("HAS_MEMBER", endNodeType = SoloArtistEntity.class)
|
||||
private List<Member> member = new ArrayList<>();
|
||||
----
|
||||
|
||||
The POJO, in this case `Member` is required to have the following structure :
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public static class Member {
|
||||
|
||||
private SoloArtistEntity artist;
|
||||
|
||||
private Year joinedIn;
|
||||
|
||||
private Year leftIn;
|
||||
}
|
||||
----
|
||||
|
||||
It must have exactly _one_ attribute of `endNodeType`.
|
||||
All other attributes are mapped from the properties of the relationship.
|
||||
|
||||
The motivation behind this is that a relationship needs to be manifested in the domain model,
|
||||
but as the domain model usually isn't a graph, it manifests itself as a thing, not a relationship as is.
|
||||
We prefer that people use the mapping framework domain centric, not database centric.
|
||||
In the relational world it is an anti pattern to map out n:m (intersection) tables.
|
||||
If they have attributes, a schema is usually refactored into a 1:n and a n:1 table and an entity structure.
|
||||
We don't need another entity, though.
|
||||
|
||||
[[labels]]
|
||||
=== Labels
|
||||
|
||||
NOTE: Do we need support for dynamic labels?
|
||||
We propose a new `@Node` annotation that takes in an array of strings as labels for that object.
|
||||
We like to get rid of `@Label` annotation supporting dynamic labels for objects
|
||||
|
||||
|
||||
|
||||
=== Integration tests
|
||||
|
||||
Integration tests take more time by their very nature.
|
||||
To get fast feedback we have split up the tests in unit and integration tests.
|
||||
Unit tests will run when the `test` goal is issued and should have a name ending with `Test` or `Tests`.
|
||||
Integration tests will get executed withing the `verify` goal and their class name have to end with `IntegrationTest` to get picked up.
|
||||
|
||||
== Configuration
|
||||
|
||||
Spring Data Neo4j RX takes a "ready to use" drivers instance and uses that.
|
||||
We won't provide any additional configuration for aspects that are configurable through the driver.
|
||||
We will however provide support to configure the drivers instance in Spring Boot.
|
||||
The current SDN Spring Boot Starter only configures the Neo4j-OGM transport and not the "real" driver.
|
||||
Our plans for a future starter a have been <<starter,described separately>>.
|
||||
|
||||
Closing the driver is not the the concern of Spring Data Neo4j RX.
|
||||
The lifecycle of that bean should be managed by the application.
|
||||
Therefore, the starter need to take care of register the drivers instance with the application.
|
||||
|
||||
== Architecture
|
||||
|
||||
This is definitely not the last version of the architecture.
|
||||
It is only meant to be a basic for discussions.
|
||||
|
||||
=== Package structure
|
||||
|
||||
.A rough outline of the current and maybe future package structure
|
||||
[plantuml, width=1200]
|
||||
----
|
||||
@startuml
|
||||
note "Implementation of Spring Data Commons SPI" as SDC_note
|
||||
package "org.neo4j.springframework.data" {
|
||||
package "core" {
|
||||
interface Neo4jClient
|
||||
interface ReactiveNeo4jClient
|
||||
package "schema" {
|
||||
package "internal" {
|
||||
note "Schema description" as schemaDescription
|
||||
}
|
||||
annotation Node
|
||||
annotation Property
|
||||
}
|
||||
package "mapping" {
|
||||
interface Neo4jPersistentEntity
|
||||
interface Neo4jPersistentProperty
|
||||
}
|
||||
package "transaction" {
|
||||
class Neo4jTransactionManager
|
||||
}
|
||||
package "convert" {
|
||||
note "conversion support" as conversionNote
|
||||
}
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
SDC_note..config
|
||||
package "config" {
|
||||
class EnableNeo4jRepository
|
||||
class Neo4jRepositoryRegistrar
|
||||
class Neo4jRepositoryConfigExtension
|
||||
}
|
||||
package "query" {
|
||||
annotation Query
|
||||
}
|
||||
package "support" {
|
||||
class Neo4jRepositoryFactoryBean
|
||||
class SimpleNeo4jRepository
|
||||
class Neo4jQueryLookupStrategy
|
||||
}
|
||||
interface Neo4jRepository
|
||||
interface ReactiveNeo4jRepository
|
||||
}
|
||||
|
||||
core-[hidden]--->repository
|
||||
}
|
||||
|
||||
@enduml
|
||||
----
|
||||
|
||||
[options="header"]
|
||||
|===
|
||||
|Package|Comment
|
||||
|core
|
||||
|`Neo4jTemplate` and related classes.
|
||||
|core.schema
|
||||
|Annotations for marking classes as nodes to be saved as well as internal schema description.
|
||||
|Infrastructure for dirty tracking etc.
|
||||
|core.mapping
|
||||
|Spring mapping information.
|
||||
|core.mapping.internal
|
||||
|Neo4j data mapping.
|
||||
|core.session
|
||||
|Connection to the `Driver` instance.
|
||||
|core.convert
|
||||
|_not used yet_ place for conversion related classes.
|
||||
|
||||
|repository
|
||||
|Repository interfaces like `Neo4jRepository`.
|
||||
|repository.config
|
||||
|Register all needed beans for Spring context.
|
||||
|repository.query
|
||||
|Place where `@Query` and other query method related annotations go in.
|
||||
|repository.support
|
||||
|"Glue code" like `Neo4jRepositoryFactoryBean`, `SimpleNeo4jRepository` etc.
|
||||
|===
|
||||
|
||||
=== Architecture validation
|
||||
The structure of this project can be explored as a Graph.
|
||||
We use https://jqassistant.org[jQAssistant] to verify our architecture during the build.
|
||||
Run the following two commands
|
||||
|
||||
```
|
||||
./mvnw clean compile jqassistant:scan
|
||||
./mvnw jqassistant:server
|
||||
```
|
||||
|
||||
and point your browser to http://localhost:7474.
|
||||
|
||||
=== `SimpleNeo4jRepository` initialization
|
||||
. `@EnableNeo4jRepositories` defines
|
||||
** the `repositoryFactoryBeanClass` that defaults to `Neo4jRepositoryFactoryBean.class`. (I)
|
||||
** `Neo4jRepositoriesRegistrar` as a configuration via the `@Import` annotation.
|
||||
. `Neo4jRepositoriesRegistrar` connects `@EnableNeo4jRepositories` with `Neo4jRepositoryConfigurationExtension`.
|
||||
. `Neo4jRepositoryConfigurationExtension` creates `Neo4jRepositoryFactoryBean` (the class defined (I)).
|
||||
** Adds manually created `Neo4jTemplate` (as an implementation of `Neo4jOperations`) bean by setting it (`setNeo4jOperations`) in the `Neo4jRepositoryFactoryBean`. (II)
|
||||
** Defines the default/fallback `RepositoryFactoryBeanClassName` as `Neo4jRepositoryFactoryBean.class.getName()` in `getRepositoryFactoryBeanClassName`.
|
||||
. `Neo4jRepositoryFactoryBean` has a super constructor that gets called from the infrastructure code.
|
||||
As a consequence the `neo4jOperations` property has to get set in (II) after initialization.
|
||||
** Creates a new instance of `Neo4jRepositoryFactory` with the in (II) provided `Neo4jOperations` in `doCreateRepositoryFactory`.
|
||||
. `Neo4jRepositoryFactory` will then create a `SimpleNeo4jRepository`.
|
||||
** It does this by calling `getTargetRepositoryViaReflection` in `getTargetRepository` and providing the `neo4jOperations`.
|
||||
. `SimpleNeo4jRepository` (the repository behind every user defined repository) is initialized.
|
||||
|
||||
=== Query execution
|
||||
|
||||
NOTE: This section contains the already straight-forward implemented support for custom queries via `@Query`.
|
||||
The other execution paths are only drafts right now and marked with a `*`.
|
||||
|
||||
`Neo4jRepositoryFactory` overrides the `getQueryLookupStrategy` method to provide the `Neo4jQueryLookupStrategy`.
|
||||
From our previous experience and handling in other Spring Data stores this would branch off in two (technical three) directions:
|
||||
|
||||
. `StringBasedNeo4jQuery` for custom Cypher queries that are provided with the `@Query` annotation.
|
||||
. `StringBasedNeo4jQuery` for named queries that are outsourced in property files.
|
||||
. `PartTreeNeo4jQuery` for derived finder methods.
|
||||
|
||||
All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jClient` and `QueryMethodEvaluationContextProvider` (not used yet) provided.
|
||||
This is a wrapper around the `java.lang.reflect.Method` passed into the `resolveQuery` method of the `Neo4jQueryLookupStrategy` to provide additional metadata.
|
||||
|
||||
==== `StringBasedNeo4jQuery` execution
|
||||
|
||||
At the moment the implementation just takes the value of the provided `@Query` annotation by calling `getAnnotatedQuery` on the `Neo4jQueryMethod`
|
||||
and executes it through the `neo4jOperations` (`Neo4jTemplate`) class.
|
||||
|
||||
=== Dirty tracking
|
||||
|
||||
We considered several approaches of dirty tracking in SDN/RX:
|
||||
|
||||
. No dirty tracking at all.
|
||||
_Not an option when it comes to relationships._
|
||||
. Dirty tracking through hashes.
|
||||
_Not on the level of detail (fields) we want to have it._
|
||||
. Using some kind of event / listener to track changes.
|
||||
. Shallow copy of objects to get compared on save.
|
||||
_A full copy of the objects will occupy twice the memory._
|
||||
|
||||
We have settled with option 1 (See ADR-004), analogue to Spring Data JDBC.
|
||||
|
||||
[[starter]]
|
||||
== Spring Data Neo4j⚡️RX Spring Boot Starter
|
||||
|
||||
The Spring Data Neo4j RX Spring Boot Starter provides automatic configuration to
|
||||
|
||||
* Create an instance of the https://github.com/neo4j/neo4j-java-driver[neo4j-java-driver]
|
||||
* Configure Spring Data Neo4j RX itself inside a Spring Boot application and enabling Spring Data repositories
|
||||
|
||||
=== Architectural guidelines and principles
|
||||
|
||||
To make a possible move into Spring Boot project itself easier,
|
||||
we don't use https://projectlombok.org[Lombok] currently in the starter as none of the official Spring Boot starters does.
|
||||
|
||||
==== Project hierarchy and dependency management
|
||||
|
||||
While the starter is a module of SDN/RX itself, it's actual parent project is `org.springframework.boot:spring-boot-starter-parent`.
|
||||
Thus we stay consistent with all other Spring Boot starters, that are actually part of Spring Boot.
|
||||
|
||||
==== Responsibilities
|
||||
|
||||
The starter and it's automatic configuration is responsible for configuring Spring Data Neo4j RX repositories and infrastructure.
|
||||
It needs a configured Neo4j Java Driver and therefor is itself dependent on `org.neo4j.driver:neo4j-java-driver-spring-boot-starter`,
|
||||
the official starter for the Neo4j Java Driver.
|
||||
|
||||
Having the starter provide automatic configuration is in accordance with the plans for Spring Data Neo4j RX.
|
||||
Spring Data Neo4j RX should only deal with configured, ready to use driver objects and not be responsible for configuring those.
|
||||
|
||||
=== Future plans
|
||||
|
||||
It would be nice having this starter here moved into https://github.com/spring-projects/spring-boot[Spring Boot] itself at some point.
|
||||
Regardless of that, we might suggest backporting `Neo4jDriverAutoConfiguration` alone to Spring Boot and enhance https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java[the existing `Neo4jDataAutoConfiguration`] to check whether there's a Driver bean available
|
||||
and if so, pass this one to OGM instead of creating a new driver.
|
||||
That would also remove the need for being able to unwrap the native driver.
|
||||
|
||||
See related discussion: https://github.com/spring-projects/spring-boot/issues/17610[Provide dedicated Neo4j driver auto-configuration].
|
||||
|
||||
|
||||
|
||||
== Open questions
|
||||
|
||||
* <<labels,Dynamic label support>>
|
||||
* Reloading nodes from the database and the affect on already loaded and changed objects.
|
||||
129
etc/checkstyle/config.xml
Normal file
129
etc/checkstyle/config.xml
Normal file
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0"?>
|
||||
<!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="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="3" />
|
||||
</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.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" />
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
<!-- System.outs -->
|
||||
<module name="Regexp">
|
||||
<property name="format" value="System\.out\.println"/>
|
||||
<property name="illegalPattern" value="true"/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
20
etc/checkstyle/java-header.txt
Normal file
20
etc/checkstyle/java-header.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
^\Q/*\E$
|
||||
^\Q * Copyright (c) 2019-2020 "Neo4j,"\E$
|
||||
^\Q * Neo4j Sweden AB [https://neo4j.com]\E$
|
||||
^\Q *\E$
|
||||
^\Q * This file is part of Neo4j.\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 .+;$
|
||||
^.*$
|
||||
7
etc/checkstyle/suppressions.xml
Normal file
7
etc/checkstyle/suppressions.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE suppressions PUBLIC
|
||||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress checks="RegexpHeader" files="package-info\.java"/>
|
||||
</suppressions>
|
||||
53
etc/jqassistant/api.adoc
Normal file
53
etc/jqassistant/api.adoc
Normal file
@@ -0,0 +1,53 @@
|
||||
[[api:Default]]
|
||||
[role=group,includesConstraints="api:*"]
|
||||
|
||||
=== General considerations
|
||||
|
||||
We use https://github.com/apiguardian-team/apiguardian[@API Guardian] to keep track of what we expose as public or internal API.
|
||||
To keep things both clear and concise, we restrict the usage of those annotations to interfaces, classes (incl. constructors)
|
||||
and annotations.
|
||||
|
||||
[[api:api-guardian-usage]]
|
||||
[source,cypher,role="constraint"]
|
||||
.@API Guardian annotations must not be used on fields
|
||||
----
|
||||
MATCH (c:Java) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}),
|
||||
(p) - [:DECLARES] -> (c)
|
||||
WHERE c:Member AND NOT c:Constructor
|
||||
RETURN p.fqn, c.name
|
||||
----
|
||||
|
||||
Public interfaces, classes or annotations are either part of internal or public API and have a status.
|
||||
|
||||
[[api:api-guardian-api-concept]]
|
||||
[source,cypher,role="concept",verify=rowCount,rowCountMin=0]
|
||||
.Define which Java artifacts are part of internal or public API
|
||||
----
|
||||
MATCH (c:Java) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}),
|
||||
(a) - [:HAS] -> ({name: 'status'}) - [:IS] -> (s)
|
||||
WHERE ANY (label IN labels(c) WHERE label in ['Interface', 'Class', 'Annotation'])
|
||||
WITH c, trim(split(s.signature, ' ')[1]) AS status
|
||||
WITH c, status,
|
||||
CASE status
|
||||
WHEN 'INTERNAL' THEN 'Internal'
|
||||
ELSE 'Public'
|
||||
END AS type
|
||||
MERGE (a:Api {type: type, status: status})
|
||||
MERGE (c) - [:IS_PART_OF] -> (a)
|
||||
RETURN c,a
|
||||
----
|
||||
|
||||
=== Internal API
|
||||
|
||||
See ADR-003.
|
||||
|
||||
[[api:internal]]
|
||||
[source,cypher,role="constraint",requiresConcepts="api:api-guardian-api-concept"]
|
||||
.Non abstract, public classes that are only part of internal API must be final
|
||||
----
|
||||
MATCH (c:Class) - [:IS_PART_OF] -> (:Api {type: 'Internal'})
|
||||
WHERE c.visibility = 'public'
|
||||
AND coalesce(c.abstract, false) = false
|
||||
AND NOT exists(c.final)
|
||||
RETURN c.name
|
||||
----
|
||||
23
etc/jqassistant/index.adoc
Normal file
23
etc/jqassistant/index.adoc
Normal file
@@ -0,0 +1,23 @@
|
||||
= Coding Rules
|
||||
|
||||
The following rules are checked during a build:
|
||||
|
||||
[[default]]
|
||||
[role=group,includesGroups="api:Default,naming:Default,structure:Default"]
|
||||
- <<api:Default>>
|
||||
- <<naming:Default>>
|
||||
- <<structure:Default>>
|
||||
|
||||
== API
|
||||
|
||||
Ensure that we publish our API in a sane and consistent way.
|
||||
|
||||
include::api.adoc[]
|
||||
|
||||
== Naming things
|
||||
|
||||
include::naming.adoc[]
|
||||
|
||||
== Structuring things
|
||||
|
||||
include::structure.adoc[]
|
||||
16
etc/jqassistant/naming.adoc
Normal file
16
etc/jqassistant/naming.adoc
Normal file
@@ -0,0 +1,16 @@
|
||||
[[naming:Default]]
|
||||
[role=group,includesConstraints="naming:TypeNameMustBeginWithGroupId"]
|
||||
|
||||
The following naming conventions are used throughout the project:
|
||||
|
||||
[[naming:TypeNameMustBeginWithGroupId]]
|
||||
[source,cypher,role=constraint]
|
||||
.All Java types must be located in packages that start with `org.neo4j.springframework.data`.
|
||||
----
|
||||
MATCH
|
||||
(project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type)
|
||||
WHERE
|
||||
NOT type.fqn starts with 'org.neo4j.springframework.data'
|
||||
RETURN
|
||||
project as Project, collect(type) as TypeWithWrongName
|
||||
----
|
||||
17
etc/jqassistant/structure.adoc
Normal file
17
etc/jqassistant/structure.adoc
Normal file
@@ -0,0 +1,17 @@
|
||||
[[structure:Default]]
|
||||
[role=group,includesConstraints="structure:mapping"]
|
||||
|
||||
Most of the time, the package structure under `org.neo4j.springframework.data` should reflect the main building parts.
|
||||
|
||||
[[structure:mapping]]
|
||||
[source,cypher,role=constraint,requiresConcepts="dependency:Package"]
|
||||
.The mapping package must not depend on any other SDN/RX packages than `schema` and `convert`
|
||||
----
|
||||
MATCH (a:Main:Artifact)
|
||||
OPTIONAL MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn in ['org.neo4j.springframework.data.core.schema', 'org.neo4j.springframework.data.core.convert']
|
||||
WITH collect(s) as allowed, a
|
||||
MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a)
|
||||
WHERE p1.fqn = 'org.neo4j.springframework.data.core.mapping'
|
||||
AND NOT (p2 in allowed OR (p1) -[:CONTAINS]-> (p2))
|
||||
return p1,p2
|
||||
----
|
||||
2
lombok.config
Normal file
2
lombok.config
Normal file
@@ -0,0 +1,2 @@
|
||||
lombok.nonNull.exceptionType = IllegalArgumentException
|
||||
|
||||
661
pom.xml
661
pom.xml
@@ -1,169 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
| Copyright 2011-2020 the original author or authors.
|
||||
| Copyright (c) 2019-2020 "Neo4j,"
|
||||
| Neo4j Sweden AB [https://neo4j.com]
|
||||
|
|
||||
| This file is part of Neo4j.
|
||||
|
|
||||
| Licensed under the Apache License, Version 2.0 (the "License");
|
||||
| you may not use this file except in compliance with the License.
|
||||
| You may obtain a copy of the License at
|
||||
|
|
||||
| https://www.apache.org/licenses/LICENSE-2.0
|
||||
| https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
||||
| Unless required by applicable law or agreed to in writing, software
|
||||
| distributed under the License is distributed on an "AS IS" BASIS,
|
||||
| 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">
|
||||
-->
|
||||
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j-parent</artifactId>
|
||||
<version>5.4.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Data Neo4j</name>
|
||||
<description>Neo4j support for Spring Data</description>
|
||||
<url>https://projects.spring.io/spring-data-neo4j</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>2.4.0-SNAPSHOT</version>
|
||||
<version>2.3.1.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
<module>spring-data-neo4j</module>
|
||||
<module>spring-data-neo4j-distribution</module>
|
||||
</modules>
|
||||
<groupId>org.neo4j.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j-rx-parent</artifactId>
|
||||
<version>${revision}${sha1}${changelist}</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<dist.id>spring-data-neo4j</dist.id>
|
||||
|
||||
<project.type>multi</project.type>
|
||||
|
||||
<neo4j.ogm.version>3.2.11</neo4j.ogm.version>
|
||||
<springdata.commons>2.4.0-SNAPSHOT</springdata.commons>
|
||||
</properties>
|
||||
<name>Spring Data Neo4j RX</name>
|
||||
<description>Next generation Object-Graph-Mapping for Spring Data.</description>
|
||||
<inceptionYear>2019</inceptionYear>
|
||||
<organization>
|
||||
<name>Neo4j, Neo4j Sweden AB</name>
|
||||
<url>https://neo4j.com</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<id>vbickers</id>
|
||||
<name>Vince Bickers</name>
|
||||
<email>vince at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>GMT</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>atg</id>
|
||||
<name>Adam George</name>
|
||||
<email>adam at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>GMT</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>bachmanm</id>
|
||||
<name>Michal Bachman</name>
|
||||
<email>michal at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>GMT</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>lmisquitta</id>
|
||||
<name>Luanne Misquitta</name>
|
||||
<email>luanne at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>+5:30</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>mangrish</id>
|
||||
<name>Mark Angrish</name>
|
||||
<email>mark at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>+11</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>jasperblues</id>
|
||||
<name>Jasper Blues</name>
|
||||
<email>jasper at graphaware.com</email>
|
||||
<organization>GraphAware</organization>
|
||||
<organizationUrl>https://www.graphaware.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>+8</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>mhunger</id>
|
||||
<name>Michael Hunger</name>
|
||||
<email>michael.hunger at neotechnology.com</email>
|
||||
<organization>Neo Technology</organization>
|
||||
<organizationUrl>https://www.neotechnology.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Project Lead</role>
|
||||
</roles>
|
||||
<timezone>+1</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>ogierke</id>
|
||||
<name>Oliver Gierke</name>
|
||||
<email>ogierke at gopivotal.com</email>
|
||||
<organization>Pivotal</organization>
|
||||
<organizationUrl>https://www.spring.io</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>+1</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>trisberg</id>
|
||||
<name>Thomas Risberg</name>
|
||||
<email>trisberg at gopivotal.com</email>
|
||||
<organization>Pivotal</organization>
|
||||
<organizationUrl>https://www.spring.io</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>-5</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>mpollack</id>
|
||||
<name>Mark Pollack</name>
|
||||
<email>mpollack at gopivotal.com</email>
|
||||
<organization>Pivotal</organization>
|
||||
<organizationUrl>https://www.spring.io</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<timezone>-5</timezone>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>gmeier</id>
|
||||
<name>Gerrit Meier</name>
|
||||
<email>gerrit.meier at neo4j.com</email>
|
||||
<organization>Neo Technology</organization>
|
||||
<organizationUrl>https://www.neotechnology.com</organizationUrl>
|
||||
<organizationUrl>http://www.neotechnology.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
<role>Project Lead</role>
|
||||
</roles>
|
||||
<timezone>+1</timezone>
|
||||
</developer>
|
||||
@@ -172,57 +63,321 @@
|
||||
<name>Michael Simons</name>
|
||||
<email>michael.simons at neo4j.com</email>
|
||||
<organization>Neo Technology</organization>
|
||||
<organizationUrl>https://www.neotechnology.com</organizationUrl>
|
||||
<organizationUrl>http://www.neotechnology.com</organizationUrl>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
<role>Project Lead</role>
|
||||
</roles>
|
||||
<timezone>+1</timezone>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jfrog.buildinfo</groupId>
|
||||
<artifactId>artifactory-maven-plugin</artifactId>
|
||||
<inherited>false</inherited>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
<modules>
|
||||
<module>spring-data-neo4j-rx</module>
|
||||
<module>spring-data-neo4j-rx-spring-boot-starter-parent</module>
|
||||
<module>examples/reactive-web</module>
|
||||
<module>examples/imperative-web</module>
|
||||
<module>examples/mapping</module>
|
||||
<module>examples/multi-database</module>
|
||||
<module>examples/rest</module>
|
||||
<module>examples/docs</module>
|
||||
<module>examples/kotlin</module>
|
||||
</modules>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-libs-snapshot</id>
|
||||
<url>https://repo.spring.io/libs-snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<properties>
|
||||
<apiguardian.version>1.1.0</apiguardian.version>
|
||||
<asciidoctorj-diagram.version>2.0.1</asciidoctorj-diagram.version>
|
||||
<asciidoctor-maven-plugin.version>1.6.0</asciidoctor-maven-plugin.version>
|
||||
<assertj.version>3.15.0</assertj.version>
|
||||
<byte-buddy.version>1.10.9</byte-buddy.version>
|
||||
<changelist>-SNAPSHOT</changelist>
|
||||
<checkstyle.version>8.29</checkstyle.version>
|
||||
<cypher-dsl.version>2020.0.0</cypher-dsl.version>
|
||||
<flatten-maven-plugin.version>1.2.1</flatten-maven-plugin.version>
|
||||
<jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version>
|
||||
<java.version>1.8</java.version>
|
||||
<java-module-name />
|
||||
<jqassistant-dashboard-plugin.version>1.8.0</jqassistant-dashboard-plugin.version>
|
||||
<jqassistant.plugin.version>1.8.0</jqassistant.plugin.version>
|
||||
<jqassistant.version>1.8.0</jqassistant.version>
|
||||
<junit-jupiter.version>5.6.1</junit-jupiter.version>
|
||||
<maven-checkstyle-plugin.version>3.1.0</maven-checkstyle-plugin.version>
|
||||
<maven-deploy-plugin.version>3.0.0-M1</maven-deploy-plugin.version>
|
||||
<maven-enforcer-plugin.version>3.0.0-M3</maven-enforcer-plugin.version>
|
||||
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
|
||||
<maven-failsafe-plugin.version>3.0.0-M4</maven-failsafe-plugin.version>
|
||||
<maven-install-plugin.version>3.0.0-M1</maven-install-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.1.1</maven-javadoc-plugin.version>
|
||||
<maven-site-plugin.version>3.7.1</maven-site-plugin.version>
|
||||
<maven-source-plugin.version>3.2.0</maven-source-plugin.version>
|
||||
<maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<mockito.version>3.2.4</mockito.version>
|
||||
<neo4j-java-driver.version>4.0.1</neo4j-java-driver.version>
|
||||
<neo4j.version>4.0.3</neo4j.version>
|
||||
<objenesis.version>3.0.1</objenesis.version> <!-- mockk requires objenesis >= 3 -->
|
||||
<project.build.docs>${project.build.directory}/docs</project.build.docs>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<r2dbc.releasetrain>Arabba-RELEASE</r2dbc.releasetrain>
|
||||
<reactive-streams.version>1.2.1</reactive-streams.version>
|
||||
<revision>1.1</revision>
|
||||
<rxjava.version>1.3.8</rxjava.version>
|
||||
<rxjava2.version>2.2.5</rxjava2.version>
|
||||
<sha1></sha1>
|
||||
<springdata-commons.version>2.3.1.RELEASE</springdata-commons.version>
|
||||
<testcontainers.version>1.13.0</testcontainers.version>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-plugins-release</id>
|
||||
<url>https://repo.spring.io/plugins-release</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
<skipUnitTests>${skipTests}</skipUnitTests>
|
||||
<skipIntegrationTests>${skipTests}</skipIntegrationTests>
|
||||
<skipArchitectureTests>${skipTests}</skipArchitectureTests>
|
||||
|
||||
<!-- For whatever reason, redeclaring those dependencies in our managed dependencies doesn't have any affect... -->
|
||||
<assertj>${assertj.version}</assertj>
|
||||
<mockito>${mockito.version}</mockito>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>${byte-buddy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy-agent</artifactId>
|
||||
<version>${byte-buddy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.objenesis</groupId>
|
||||
<artifactId>objenesis</artifactId>
|
||||
<version>${objenesis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>${rxjava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava-reactive-streams</artifactId>
|
||||
<version>${reactive-streams.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>${rxjava2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-cypher-dsl</artifactId>
|
||||
<version>${cypher-dsl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apiguardian</groupId>
|
||||
<artifactId>apiguardian-api</artifactId>
|
||||
<version>${apiguardian.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assertj.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>${junit-jupiter.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-bom</artifactId>
|
||||
<version>${r2dbc.releasetrain}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j.driver</groupId>
|
||||
<artifactId>neo4j-java-driver</artifactId>
|
||||
<version>${neo4j-java-driver.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j.test</groupId>
|
||||
<artifactId>neo4j-harness</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
<version>${springdata-commons.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.ekryd.sortpom</groupId>
|
||||
<artifactId>sortpom-maven-plugin</artifactId>
|
||||
<version>2.8.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sort</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<keepBlankLines>true</keepBlankLines>
|
||||
<nrOfIndentSpace>-1</nrOfIndentSpace>
|
||||
<sortProperties>true</sortProperties>
|
||||
<sortDependencies>groupId,artifactId</sortDependencies>
|
||||
<createBackupFile>false</createBackupFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${maven-checkstyle-plugin.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>${checkstyle.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<configLocation>etc/checkstyle/config.xml</configLocation>
|
||||
<suppressionsLocation>etc/checkstyle/suppressions.xml</suppressionsLocation>
|
||||
<headerLocation>etc/checkstyle/java-header.txt</headerLocation>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.buschmais.jqassistant</groupId>
|
||||
<artifactId>jqassistant-maven-plugin</artifactId>
|
||||
<version>${jqassistant.version}</version>
|
||||
<configuration>
|
||||
<rulesDirectory>etc/jqassistant</rulesDirectory>
|
||||
<skip>${skipArchitectureTests}</skip>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jqassistant.contrib.plugin</groupId>
|
||||
<artifactId>jqassistant-dashboard-plugin</artifactId>
|
||||
<version>${jqassistant-dashboard-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.kontext-e.jqassistant.plugin</groupId>
|
||||
<artifactId>jqassistant.plugin.git</artifactId>
|
||||
<version>${jqassistant.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>${maven-deploy-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>${maven-enforcer-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>${maven-install-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${maven-failsafe-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-maven-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<!-- See https://github.com/awhitford/lombok.maven/issues/34 -->
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>${flatten-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<configuration combine.self="override">
|
||||
<configLocation>checkstyle/config.xml</configLocation>
|
||||
<suppressionsLocation>checkstyle/suppressions.xml</suppressionsLocation>
|
||||
<headerLocation>checkstyle/java-header.txt</headerLocation>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
@@ -233,7 +388,159 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<append>true</append>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>prepare-agent-integration</id>
|
||||
<goals>
|
||||
<goal>prepare-agent-integration</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<append>true</append>
|
||||
<destFile>${project.build.directory}/jacoco.exec</destFile>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<DependencyConvergence></DependencyConvergence>
|
||||
<requireMavenVersion>
|
||||
<version>3.6.0</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<useSystemClassLoader>false</useSystemClassLoader>
|
||||
<useFile>false</useFile>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
<include>**/*Tests.java</include>
|
||||
</includes>
|
||||
<skipTests>${skipUnitTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>${skipIntegrationTests}</skipTests>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>resolveCiFriendliesOnly</flattenMode>
|
||||
</configuration>
|
||||
<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>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
<addBuildEnvironmentEntries>true</addBuildEnvironmentEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>${java-module-name}</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
<version>${asciidoctor-maven-plugin.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctorj-diagram</artifactId>
|
||||
<version>${asciidoctorj-diagram.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<backend>html</backend>
|
||||
<doctype>book</doctype>
|
||||
<imagesDir>img</imagesDir>
|
||||
<sourceDirectory>${basedir}/docs</sourceDirectory>
|
||||
<sourceDocumentName>index.adoc</sourceDocumentName>
|
||||
<sourceHighlighter>coderay</sourceHighlighter>
|
||||
<attributes>
|
||||
<icons>font</icons>
|
||||
<toc>left</toc>
|
||||
<setanchors></setanchors>
|
||||
<idprefix></idprefix>
|
||||
<idseparator></idseparator>
|
||||
</attributes>
|
||||
<requires>
|
||||
<require>asciidoctor-diagram</require>
|
||||
</requires>
|
||||
<outputDirectory>${project.build.docs}</outputDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-docs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>process-asciidoc</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
| Copyright 2011-2020 the original author or authors.
|
||||
| Copyright (c) 2019-2020 "Neo4j,"
|
||||
| Neo4j Sweden AB [https://neo4j.com]
|
||||
|
|
||||
| This file is part of Neo4j.
|
||||
|
|
||||
| Licensed under the Apache License, Version 2.0 (the "License");
|
||||
| you may not use this file except in compliance with the License.
|
||||
| You may obtain a copy of the License at
|
||||
|
|
||||
| https://www.apache.org/licenses/LICENSE-2.0
|
||||
| https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
||||
| Unless required by applicable law or agreed to in writing, software
|
||||
| distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -14,215 +17,168 @@
|
||||
| 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">
|
||||
|
||||
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
|
||||
<name>Spring Data Neo4J - Core</name>
|
||||
<description>Neo4J support for Spring Data</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j-parent</artifactId>
|
||||
<version>5.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
<artifactId>spring-data-neo4j-rx-parent</artifactId>
|
||||
<groupId>org.neo4j.springframework.data</groupId>
|
||||
<version>${revision}${sha1}${changelist}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-data-neo4j-rx</artifactId>
|
||||
|
||||
<name>SDN⚡️RX</name>
|
||||
<description>Core module of SDN.rx.</description>
|
||||
|
||||
<properties>
|
||||
<project.root>${basedir}/..</project.root>
|
||||
|
||||
<java-module-name>spring.data.neo4j</java-module-name>
|
||||
|
||||
<caffeine.version>2.6.2</caffeine.version>
|
||||
<el-api.version>2.2</el-api.version>
|
||||
<javax-jaxb.version>2.3.1</javax-jaxb.version>
|
||||
<neo4j.version>3.5.18</neo4j.version>
|
||||
<ogm.properties>ogm-bolt.properties</ogm.properties>
|
||||
<spotbugs-maven-plugin.version>3.1.3</spotbugs-maven-plugin.version>
|
||||
<spotbugs.version>3.1.3</spotbugs.version>
|
||||
<java-module-name>spring.data.neo4j.rx</java-module-name>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava-reactive-streams</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.transaction</groupId>
|
||||
<artifactId>jta</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-r2dbc</artifactId>
|
||||
<version>1.0.0.RELEASE</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-cypher-dsl</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apiguardian</groupId>
|
||||
<artifactId>apiguardian-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j.driver</groupId>
|
||||
<artifactId>neo4j-java-driver</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-beans</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Data -->
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
<version>${springdata.commons}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- CDI -->
|
||||
<!-- Dependency order required to build against CDI 1.0 and test with CDI 2.0 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.geronimo.specs</groupId>
|
||||
<artifactId>geronimo-jcdi_2.0_spec</artifactId>
|
||||
<version>1.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.interceptor</groupId>
|
||||
<artifactId>javax.interceptor-api</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.enterprise</groupId>
|
||||
<artifactId>cdi-api</artifactId>
|
||||
<version>${cdi}</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<version>${javax-annotation-api}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.openwebbeans</groupId>
|
||||
<artifactId>openwebbeans-se</artifactId>
|
||||
<version>${webbeans}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- JSR 303 Validation -->
|
||||
<dependency>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
<version>${validation}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>5.1.2.Final</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.el</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Cache -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>${caffeine.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Neo4j OGM -->
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-ogm-core</artifactId>
|
||||
<version>${neo4j.ogm.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-ogm-api</artifactId>
|
||||
<version>${neo4j.ogm.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-ogm-bolt-driver</artifactId>
|
||||
<version>${neo4j.ogm.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-ogm-bolt-native-types</artifactId>
|
||||
<version>${neo4j.ogm.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.neo4j.test</groupId>
|
||||
<artifactId>neo4j-harness</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-library</artifactId>
|
||||
<version>${hamcrest}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
<version>${hamcrest}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin}</version>
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-coroutines-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-coroutines-reactor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.mockk</groupId>
|
||||
<artifactId>mockk</artifactId>
|
||||
<version>${mockk}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>eu.michael-simons.neo4j</groupId>
|
||||
<artifactId>junit-jupiter-causal-cluster-testcontainer-extension</artifactId>
|
||||
<version>4.0.2.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Reduce scope of Lombok to test (it is defined by Spring Data Parent) -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok}</version>
|
||||
<optional>true</optional>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -230,58 +186,73 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>java-test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<id>report</id>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<compilerArgs>-parameters</compilerArgs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
<version>${spotbugs-maven-plugin.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs</artifactId>
|
||||
<version>${spotbugs.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
</configuration>
|
||||
<groupId>com.buschmais.jqassistant</groupId>
|
||||
<artifactId>jqassistant-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>jqassistant-scan</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>scan</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<scanProperties>
|
||||
<jqassistant.plugin.jacoco.filename>jacoco.xml</jqassistant.plugin.jacoco.filename>
|
||||
</scanProperties>
|
||||
<scanIncludes>
|
||||
<scanInclude>
|
||||
<path>${project.basedir}/../.git</path>
|
||||
</scanInclude>
|
||||
<scanInclude>
|
||||
<path>${project.reporting.outputDirectory}/jacoco</path>
|
||||
</scanInclude>
|
||||
</scanIncludes>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>jqassistant-analyze</id>
|
||||
<goals>
|
||||
<goal>analyze</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<failOnSeverity>MINOR</failOnSeverity>
|
||||
<groups>
|
||||
<group>default</group>
|
||||
<group>jqassistant-dashboard:Default</group>
|
||||
</groups>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>jdk11</id>
|
||||
<activation>
|
||||
<jdk>11</jdk>
|
||||
</activation>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${javax-jaxb.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>${javax-jaxb.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.buschmais.jqassistant</groupId>
|
||||
<artifactId>jqassistant-maven-plugin</artifactId>
|
||||
<version>${jqassistant.version}</version>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<reports>
|
||||
<report>report</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient;
|
||||
import org.neo4j.springframework.data.core.Neo4jTemplate;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager;
|
||||
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
|
||||
import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* Base class for imperative SDN-RX 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
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@Import(Neo4jDefaultCallbacksRegistrar.class)
|
||||
public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME)
|
||||
public Neo4jClient neo4jClient(Driver driver) {
|
||||
return Neo4jClient.create(driver);
|
||||
}
|
||||
|
||||
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
|
||||
public Neo4jTemplate neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext,
|
||||
DatabaseSelectionProvider databaseNameProvider) {
|
||||
|
||||
return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @param databaseNameProvider The configured database name provider
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public PlatformTransactionManager transactionManager(Driver driver,
|
||||
DatabaseSelectionProvider databaseNameProvider) {
|
||||
|
||||
return new Neo4jTransactionManager(driver, databaseNameProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Bean
|
||||
protected DatabaseSelectionProvider neo4jDatabaseNameProvider() {
|
||||
|
||||
return DatabaseSelectionProvider.getDefaultSelectionProvider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.core.ReactiveNeo4jClient;
|
||||
import org.neo4j.springframework.data.core.ReactiveDatabaseSelectionProvider;
|
||||
import org.neo4j.springframework.data.core.ReactiveNeo4jTemplate;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager;
|
||||
import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.ReactiveTransactionManager;
|
||||
|
||||
/**
|
||||
* Base class for reactive SDN-RX 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
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@Import(Neo4jDefaultReactiveCallbacksRegistrar.class)
|
||||
public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME)
|
||||
public ReactiveNeo4jClient neo4jClient(Driver driver) {
|
||||
return ReactiveNeo4jClient.create(driver);
|
||||
}
|
||||
|
||||
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
|
||||
public ReactiveNeo4jTemplate neo4jTemplate(final ReactiveNeo4jClient neo4jClient,
|
||||
final Neo4jMappingContext mappingContext, final ReactiveDatabaseSelectionProvider databaseNameProvider) {
|
||||
|
||||
return new ReactiveNeo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseNameProvider) {
|
||||
|
||||
return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Bean
|
||||
protected ReactiveDatabaseSelectionProvider reactiveNeo4jDatabaseNameProvider() {
|
||||
|
||||
return ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.auditing.DateTimeProvider;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
|
||||
/**
|
||||
* Annotation to enable auditing for SDN-RX entities via annotation configuration.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
* @soundtrack Iron Maiden - Killers
|
||||
*/
|
||||
@Inherited
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Import(Neo4jAuditingRegistrar.class)
|
||||
public @interface EnableNeo4jAuditing {
|
||||
|
||||
/**
|
||||
* Configures the {@link AuditorAware} bean to be used to lookup the current principal.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String auditorAwareRef() default "";
|
||||
|
||||
/**
|
||||
* Configures whether the creation and modification dates are set. Defaults to {@literal true}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean setDates() default true;
|
||||
|
||||
/**
|
||||
* Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
String dateTimeProviderRef() default "";
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.neo4j.springframework.data.repository.event.AuditingBeforeBindCallback;
|
||||
import org.neo4j.springframework.data.repository.event.ReactiveAuditingBeforeBindCallback;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
|
||||
import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
|
||||
import org.springframework.data.auditing.config.AuditingConfiguration;
|
||||
import org.springframework.data.config.ParsingUtils;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Iron Maiden - Killers
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
|
||||
|
||||
private static final boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono",
|
||||
Neo4jAuditingRegistrar.class.getClassLoader());
|
||||
|
||||
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#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
|
||||
*/
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
|
||||
|
||||
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
|
||||
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
|
||||
|
||||
super.registerBeanDefinitions(annotationMetadata, registry);
|
||||
}
|
||||
|
||||
/*
|
||||
* (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) {
|
||||
|
||||
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));
|
||||
|
||||
registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(),
|
||||
AuditingBeforeBindCallback.class.getName(), registry);
|
||||
|
||||
if (PROJECT_REACTOR_AVAILABLE) {
|
||||
registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (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(IsNewAwareAuditingHandler.class);
|
||||
|
||||
BeanDefinitionBuilder persistentEntities = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(PersistentEntities.class)
|
||||
.setFactoryMethod("of");
|
||||
persistentEntities.addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME);
|
||||
|
||||
builder.addConstructorArgValue(persistentEntities.getBeanDefinition());
|
||||
return configureDefaultAuditHandlerAttributes(configuration, builder);
|
||||
}
|
||||
|
||||
private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) {
|
||||
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class);
|
||||
|
||||
builder.addConstructorArgValue(
|
||||
ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
|
||||
builder.getRawBeanDefinition().setSource(source);
|
||||
|
||||
registerInfrastructureBeanWithId(builder.getBeanDefinition(),
|
||||
ReactiveAuditingBeforeBindCallback.class.getName(), registry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
abstract class Neo4jConfigurationSupport {
|
||||
|
||||
@Bean
|
||||
public Neo4jConversions neo4jConversions() {
|
||||
return new Neo4jConversions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} equipped with entity classes
|
||||
* scanned from the mapping base package.
|
||||
*
|
||||
* @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set.
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
@Bean
|
||||
public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException {
|
||||
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions);
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected Collection<String> getMappingBasePackages() {
|
||||
|
||||
Package mappingBasePackage = getClass().getPackage();
|
||||
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
protected final Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
for (String basePackage : getMappingBasePackages()) {
|
||||
initialEntitySet.addAll(scanForEntities(basePackage));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return found entities in the package to scan.
|
||||
* @throws ClassNotFoundException if the given class cannot be loaded by the class loader.
|
||||
*/
|
||||
protected final Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
|
||||
|
||||
if (!StringUtils.hasText(basePackage)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
ClassPathScanningCandidateComponentProvider componentProvider =
|
||||
new ClassPathScanningCandidateComponentProvider(false);
|
||||
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class));
|
||||
|
||||
ClassLoader classLoader = Neo4jConfigurationSupport.class.getClassLoader();
|
||||
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
|
||||
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.repository.event.IdGeneratingBeforeBindCallback;
|
||||
import org.neo4j.springframework.data.repository.event.OptimisticLockingBeforeBindCallback;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* This brings in the default callbacks required for the default implementation of {@link org.neo4j.springframework.data.core.Neo4jOperations} to work.
|
||||
* The offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot autoconfiguration.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack AC/DC - High Voltage
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class Neo4jDefaultCallbacksRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(
|
||||
AnnotationMetadata importingClassMetadata,
|
||||
BeanDefinitionRegistry registry,
|
||||
BeanNameGenerator beanNameGenerator
|
||||
) {
|
||||
// Id Generator
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(IdGeneratingBeforeBindCallback.class);
|
||||
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
|
||||
// Optimistic locking support
|
||||
beanDefinition = new RootBeanDefinition(OptimisticLockingBeforeBindCallback.class);
|
||||
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.repository.event.ReactiveIdGeneratingBeforeBindCallback;
|
||||
import org.neo4j.springframework.data.repository.event.ReactiveOptimisticLockingBeforeBindCallback;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* This brings in the default callbacks required for the default implementation of {@link org.neo4j.springframework.data.core.Neo4jOperations} to work.
|
||||
* The offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot autoconfiguration.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack AC/DC - High Voltage
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class Neo4jDefaultReactiveCallbacksRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(
|
||||
AnnotationMetadata importingClassMetadata,
|
||||
BeanDefinitionRegistry registry,
|
||||
BeanNameGenerator beanNameGenerator
|
||||
) {
|
||||
// Id Generator
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(ReactiveIdGeneratingBeforeBindCallback.class);
|
||||
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
|
||||
// Optimistic locking support
|
||||
beanDefinition = new RootBeanDefinition(ReactiveOptimisticLockingBeforeBindCallback.class);
|
||||
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This package contains configuration related support classes that can be used for the application specific
|
||||
* {@link org.springframework.context.annotation.Configuration}.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.config;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A value holder indicating a database selection based on a optional name.
|
||||
* {@literal null} indicates to let the server decide.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Rage - Reign Of Fear
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class DatabaseSelection {
|
||||
|
||||
private final static DatabaseSelection DEFAULT_DATABASE_NAME = new DatabaseSelection(null);
|
||||
|
||||
@Nullable private final String value;
|
||||
|
||||
public static DatabaseSelection undecided() {
|
||||
|
||||
return DEFAULT_DATABASE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database selection by the given databaseName.
|
||||
*
|
||||
* @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(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DatabaseSelection that = (DatabaseSelection) o;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.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.
|
||||
* <p>An instance of a database name provider is only relevant when SDN-RX 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.
|
||||
* <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.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack N.W.A. - Straight Outta Compton
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@FunctionalInterface
|
||||
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.
|
||||
*/
|
||||
static DatabaseSelectionProvider createStaticDatabaseSelectionProvider(String databaseName) {
|
||||
|
||||
Assert.notNull(databaseName, "The database name must not be null.");
|
||||
Assert.hasText(databaseName, "The database name must not be empty.");
|
||||
|
||||
return () -> DatabaseSelection.byName(databaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A database selection provider always returning the default selection.
|
||||
*
|
||||
* @return A provider for the default database name.
|
||||
*/
|
||||
static DatabaseSelectionProvider getDefaultSelectionProvider() {
|
||||
|
||||
return DefaultDatabaseSelectionProvider.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
enum DefaultDatabaseSelectionProvider implements DatabaseSelectionProvider {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public DatabaseSelection getDatabaseSelection() {
|
||||
return DatabaseSelection.undecided();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.neo4j.springframework.data.core.Neo4jClient.*;
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*;
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.QueryRunner;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Result;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.repository.support.Neo4jPersistenceExceptionTranslator;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database.
|
||||
* TODO Micrometer hooks for statement results...
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultNeo4jClient implements Neo4jClient {
|
||||
|
||||
private final Driver driver;
|
||||
private final TypeSystem typeSystem;
|
||||
private final ConversionService conversionService;
|
||||
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
|
||||
|
||||
DefaultNeo4jClient(Driver driver) {
|
||||
|
||||
this.driver = driver;
|
||||
this.typeSystem = driver.defaultTypeSystem();
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
new Neo4jConversions().registerConvertersIn((ConverterRegistry) conversionService);
|
||||
}
|
||||
|
||||
AutoCloseableQueryRunner getQueryRunner(@Nullable final String targetDatabase) {
|
||||
|
||||
QueryRunner queryRunner = retrieveTransaction(driver, targetDatabase);
|
||||
if (queryRunner == null) {
|
||||
queryRunner = driver.session(defaultSessionConfig(targetDatabase));
|
||||
}
|
||||
|
||||
return (AutoCloseableQueryRunner) Proxy.newProxyInstance(this.getClass().getClassLoader(),
|
||||
new Class<?>[] { AutoCloseableQueryRunner.class },
|
||||
new AutoCloseableQueryRunnerHandler(queryRunner));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a query runner automatically closeable and aware whether it's session or a transaction
|
||||
*/
|
||||
interface AutoCloseableQueryRunner extends QueryRunner, AutoCloseable {
|
||||
|
||||
@Override void close();
|
||||
}
|
||||
|
||||
static class AutoCloseableQueryRunnerHandler implements InvocationHandler {
|
||||
|
||||
private final Map<Method, MethodHandle> cachedHandles = new ConcurrentHashMap<>();
|
||||
private final QueryRunner target;
|
||||
|
||||
AutoCloseableQueryRunnerHandler(QueryRunner target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
|
||||
if ("close".equals(method.getName())) {
|
||||
if (this.target instanceof Session) {
|
||||
((Session) this.target).close();
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return cachedHandles.computeIfAbsent(method, this::findHandleFor).invokeWithArguments(args);
|
||||
}
|
||||
}
|
||||
|
||||
MethodHandle findHandleFor(Method method) {
|
||||
try {
|
||||
return MethodHandles.publicLookup().unreflect(method).bindTo(target);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Below are all the implementations (methods and classes) as defined by the contracts of Neo4jClient
|
||||
|
||||
@Override
|
||||
public RunnableSpec query(String cypher) {
|
||||
return query(() -> cypher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpec query(Supplier<String> cypherSupplier) {
|
||||
return new DefaultRunnableSpec(cypherSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> OngoingDelegation<T> delegateTo(Function<QueryRunner, Optional<T>> callback) {
|
||||
return new DefaultRunnableDelegation<>(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class RunnableStatement {
|
||||
|
||||
RunnableStatement(Supplier<String> cypherSupplier) {
|
||||
this(cypherSupplier, new NamedParameters());
|
||||
}
|
||||
|
||||
RunnableStatement(Supplier<String> cypherSupplier, NamedParameters parameters) {
|
||||
this.cypherSupplier = cypherSupplier;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
private final Supplier<String> cypherSupplier;
|
||||
|
||||
private final NamedParameters parameters;
|
||||
|
||||
protected final Result runWith(AutoCloseableQueryRunner statementRunner) {
|
||||
String statementTemplate = 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));
|
||||
}
|
||||
}
|
||||
|
||||
return statementRunner.run(statementTemplate, 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
|
||||
*/
|
||||
private static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex,
|
||||
PersistenceExceptionTranslator exceptionTranslator) {
|
||||
RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex);
|
||||
return resolved == null ? ex : resolved;
|
||||
}
|
||||
|
||||
class DefaultRunnableSpec implements RunnableSpec {
|
||||
|
||||
private RunnableStatement runnableStatement;
|
||||
|
||||
private String targetDatabase;
|
||||
|
||||
DefaultRunnableSpec(Supplier<String> cypherSupplier) {
|
||||
this.runnableStatement = new RunnableStatement(cypherSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) {
|
||||
|
||||
this.targetDatabase = verifyDatabaseName(targetDatabase);
|
||||
return this;
|
||||
}
|
||||
|
||||
class DefaultOngoingBindSpec<T> implements OngoingBindSpec<T, RunnableSpecTightToDatabase> {
|
||||
|
||||
@Nullable
|
||||
private final T value;
|
||||
|
||||
DefaultOngoingBindSpec(@Nullable T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase to(String name) {
|
||||
|
||||
DefaultRunnableSpec.this.runnableStatement.parameters.add(name, value);
|
||||
return DefaultRunnableSpec.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase with(Function<T, Map<String, Object>> binder) {
|
||||
|
||||
Assert.notNull(binder, "Binder is required.");
|
||||
|
||||
return bindAll(binder.apply(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OngoingBindSpec<?, RunnableSpecTightToDatabase> bind(@Nullable Object value) {
|
||||
return new DefaultOngoingBindSpec(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase bindAll(Map<String, Object> newParameters) {
|
||||
this.runnableStatement.parameters.addAll(newParameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> MappingSpec<T> fetchAs(Class<T> targetClass) {
|
||||
|
||||
return new DefaultRecordFetchSpec(this.targetDatabase, this.runnableStatement,
|
||||
new SingleValueMappingFunction(conversionService, targetClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordFetchSpec<Map<String, Object>> fetch() {
|
||||
|
||||
return new DefaultRecordFetchSpec<>(
|
||||
this.targetDatabase,
|
||||
this.runnableStatement, (t, r) -> r.asMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultSummary run() {
|
||||
|
||||
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
return result.consume();
|
||||
} catch (RuntimeException e) {
|
||||
throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultRecordFetchSpec<T> implements RecordFetchSpec<T>, MappingSpec<T> {
|
||||
|
||||
private final String targetDatabase;
|
||||
|
||||
private final RunnableStatement runnableStatement;
|
||||
|
||||
private BiFunction<TypeSystem, Record, T> mappingFunction;
|
||||
|
||||
DefaultRecordFetchSpec(String targetDatabase, RunnableStatement runnableStatement,
|
||||
BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
this.targetDatabase = targetDatabase;
|
||||
this.runnableStatement = runnableStatement;
|
||||
this.mappingFunction = mappingFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordFetchSpec<T> mappedBy(
|
||||
@SuppressWarnings("HiddenField") BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
|
||||
this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> one() {
|
||||
|
||||
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
return result.hasNext() ?
|
||||
Optional.of(mappingFunction.apply(typeSystem, result.single())) :
|
||||
Optional.empty();
|
||||
} catch (RuntimeException e) {
|
||||
throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> first() {
|
||||
|
||||
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
return result.stream().map(partialMappingFunction(typeSystem)).findFirst();
|
||||
} catch (RuntimeException e) {
|
||||
throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> all() {
|
||||
|
||||
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
|
||||
Result result = runnableStatement.runWith(statementRunner);
|
||||
return result.stream().map(partialMappingFunction(typeSystem)).collect(toList());
|
||||
} catch (RuntimeException e) {
|
||||
throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultRunnableDelegation<T> implements RunnableDelegation<T>, OngoingDelegation<T> {
|
||||
|
||||
private final Function<QueryRunner, Optional<T>> callback;
|
||||
|
||||
@Nullable private String targetDatabase;
|
||||
|
||||
DefaultRunnableDelegation(Function<QueryRunner, Optional<T>> callback) {
|
||||
this(callback, null);
|
||||
}
|
||||
|
||||
DefaultRunnableDelegation(Function<QueryRunner, Optional<T>> callback, @Nullable String targetDatabase) {
|
||||
this.callback = callback;
|
||||
this.targetDatabase = targetDatabase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) {
|
||||
|
||||
this.targetDatabase = verifyDatabaseName(targetDatabase);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> run() {
|
||||
try (AutoCloseableQueryRunner queryRunner = getQueryRunner(targetDatabase)) {
|
||||
return callback.apply(queryRunner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static org.neo4j.springframework.data.core.Neo4jClient.*;
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
import static org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager.*;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.reactive.RxQueryRunner;
|
||||
import org.neo4j.driver.reactive.RxResult;
|
||||
import org.neo4j.driver.reactive.RxSession;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.*;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.repository.support.Neo4jPersistenceExceptionTranslator;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Reactive variant of the {@link Neo4jClient}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @soundtrack Die Toten Hosen - Im Auftrag des Herrn
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
|
||||
|
||||
private final Driver driver;
|
||||
private final TypeSystem typeSystem;
|
||||
private final ConversionService conversionService;
|
||||
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
|
||||
|
||||
DefaultReactiveNeo4jClient(Driver driver) {
|
||||
|
||||
this.driver = driver;
|
||||
this.typeSystem = driver.defaultTypeSystem();
|
||||
this.conversionService = new DefaultConversionService();
|
||||
new Neo4jConversions().registerConvertersIn((ConverterRegistry) conversionService);
|
||||
}
|
||||
|
||||
Mono<RxStatementRunnerHolder> retrieveRxStatementRunnerHolder(String targetDatabase) {
|
||||
|
||||
return retrieveReactiveTransaction(driver, targetDatabase)
|
||||
.map(rxTransaction -> new RxStatementRunnerHolder(rxTransaction, Mono.empty(), Mono.empty())) //
|
||||
.switchIfEmpty(
|
||||
Mono.using(() -> driver.rxSession(defaultSessionConfig(targetDatabase)),
|
||||
session -> Mono.from(session.beginTransaction())
|
||||
.map(tx -> new RxStatementRunnerHolder(tx, tx.commit(), tx.rollback())), RxSession::close)
|
||||
);
|
||||
}
|
||||
|
||||
<T> Mono<T> doInQueryRunnerForMono(final String targetDatabase, Function<RxQueryRunner, Mono<T>> func) {
|
||||
|
||||
return Mono.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase),
|
||||
holder -> func.apply(holder.getRxQueryRunner()),
|
||||
RxStatementRunnerHolder::getCommit,
|
||||
(holder, ex) -> holder.getRollback(),
|
||||
RxStatementRunnerHolder::getCommit);
|
||||
}
|
||||
|
||||
<T> Flux<T> doInStatementRunnerForFlux(final String targetDatabase, Function<RxQueryRunner, Flux<T>> func) {
|
||||
|
||||
return Flux.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase),
|
||||
holder -> func.apply(holder.getRxQueryRunner()),
|
||||
RxStatementRunnerHolder::getCommit,
|
||||
(holder, ex) -> holder.getRollback(),
|
||||
RxStatementRunnerHolder::getCommit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpec query(String cypher) {
|
||||
return query(() -> cypher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpec query(Supplier<String> cypherSupplier) {
|
||||
return new DefaultRunnableSpec(cypherSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> OngoingDelegation<T> delegateTo(Function<RxQueryRunner, Mono<T>> callback) {
|
||||
return new DefaultRunnableDelegation<>(callback);
|
||||
}
|
||||
|
||||
class DefaultRunnableSpec implements RunnableSpec {
|
||||
|
||||
private final Supplier<String> cypherSupplier;
|
||||
|
||||
private String targetDatabase;
|
||||
|
||||
private final NamedParameters parameters = new NamedParameters();
|
||||
|
||||
DefaultRunnableSpec(Supplier<String> cypherSupplier) {
|
||||
this.cypherSupplier = cypherSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) {
|
||||
|
||||
this.targetDatabase = verifyDatabaseName(targetDatabase);
|
||||
return this;
|
||||
}
|
||||
|
||||
class DefaultOngoingBindSpec<T> implements OngoingBindSpec<T, RunnableSpecTightToDatabase> {
|
||||
|
||||
@Nullable
|
||||
private final T value;
|
||||
|
||||
DefaultOngoingBindSpec(@Nullable T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase to(String name) {
|
||||
|
||||
DefaultRunnableSpec.this.parameters.add(name, value);
|
||||
return DefaultRunnableSpec.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase with(Function<T, Map<String, Object>> binder) {
|
||||
|
||||
Assert.notNull(binder, "Binder is required.");
|
||||
|
||||
return bindAll(binder.apply(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OngoingBindSpec<?, RunnableSpecTightToDatabase> bind(@Nullable Object value) {
|
||||
return new DefaultOngoingBindSpec(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableSpecTightToDatabase bindAll(Map<String, Object> newParameters) {
|
||||
this.parameters.addAll(newParameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> MappingSpec<R> fetchAs(Class<R> targetClass) {
|
||||
|
||||
return new DefaultRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters,
|
||||
new SingleValueMappingFunction(conversionService, targetClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordFetchSpec<Map<String, Object>> fetch() {
|
||||
|
||||
return new DefaultRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters,
|
||||
(t, r) -> r.asMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResultSummary> run() {
|
||||
|
||||
return new DefaultRecordFetchSpec<>(
|
||||
this.targetDatabase,
|
||||
this.cypherSupplier,
|
||||
this.parameters).run();
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultRecordFetchSpec<T> implements RecordFetchSpec<T>, MappingSpec<T> {
|
||||
|
||||
private final String targetDatabase;
|
||||
|
||||
private final Supplier<String> cypherSupplier;
|
||||
|
||||
private final NamedParameters parameters;
|
||||
|
||||
private BiFunction<TypeSystem, Record, T> mappingFunction;
|
||||
|
||||
DefaultRecordFetchSpec(String targetDatabase, Supplier<String> cypherSupplier,
|
||||
NamedParameters parameters) {
|
||||
this(targetDatabase, cypherSupplier, parameters, null);
|
||||
}
|
||||
|
||||
DefaultRecordFetchSpec(
|
||||
String targetDatabase, Supplier<String> cypherSupplier, NamedParameters parameters,
|
||||
@Nullable BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
this.targetDatabase = targetDatabase;
|
||||
this.cypherSupplier = cypherSupplier;
|
||||
this.parameters = parameters;
|
||||
this.mappingFunction = mappingFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordFetchSpec<T> mappedBy(BiFunction<TypeSystem, Record, T> mappingFunction) {
|
||||
|
||||
this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction);
|
||||
return this;
|
||||
}
|
||||
|
||||
Mono<Tuple2<String, Map<String, Object>>> prepareStatement() {
|
||||
if (cypherLog.isDebugEnabled()) {
|
||||
String cypher = 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));
|
||||
}
|
||||
}
|
||||
return Mono.fromSupplier(cypherSupplier).zipWith(Mono.just(parameters.get()));
|
||||
}
|
||||
|
||||
Flux<T> executeWith(Tuple2<String, Map<String, Object>> t, RxQueryRunner runner) {
|
||||
|
||||
return Flux.from(runner.run(t.getT1(), t.getT2()).records()).map(r -> mappingFunction.apply(typeSystem, r));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> one() {
|
||||
|
||||
return doInQueryRunnerForMono(
|
||||
targetDatabase,
|
||||
(runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty()
|
||||
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> first() {
|
||||
|
||||
return doInQueryRunnerForMono(
|
||||
targetDatabase,
|
||||
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next()
|
||||
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<T> all() {
|
||||
|
||||
return doInStatementRunnerForFlux(
|
||||
targetDatabase,
|
||||
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner))
|
||||
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
|
||||
}
|
||||
|
||||
Mono<ResultSummary> run() {
|
||||
|
||||
return doInQueryRunnerForMono(
|
||||
targetDatabase,
|
||||
runner -> prepareStatement().flatMap(t -> {
|
||||
RxResult rxResult = runner.run(t.getT1(), t.getT2());
|
||||
return Flux.from(rxResult.records()).then(Mono.from(rxResult.consume()));
|
||||
})
|
||||
).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
|
||||
*/
|
||||
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<RxQueryRunner, Mono<T>> callback;
|
||||
|
||||
private String targetDatabase;
|
||||
|
||||
DefaultRunnableDelegation(Function<RxQueryRunner, Mono<T>> callback) {
|
||||
this(callback, null);
|
||||
}
|
||||
|
||||
DefaultRunnableDelegation(Function<RxQueryRunner, Mono<T>> callback,
|
||||
@Nullable String targetDatabase) {
|
||||
this.callback = callback;
|
||||
this.targetDatabase = targetDatabase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) {
|
||||
|
||||
this.targetDatabase = verifyDatabaseName(targetDatabase);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> run() {
|
||||
|
||||
return doInQueryRunnerForMono(
|
||||
targetDatabase,
|
||||
callback
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final class RxStatementRunnerHolder {
|
||||
private final RxQueryRunner rxQueryRunner;
|
||||
|
||||
private final Publisher<Void> commit;
|
||||
private final Publisher<Void> rollback;
|
||||
|
||||
RxStatementRunnerHolder(RxQueryRunner rxQueryRunner, Publisher<Void> commit, Publisher<Void> rollback) {
|
||||
this.rxQueryRunner = rxQueryRunner;
|
||||
this.commit = commit;
|
||||
this.rollback = rollback;
|
||||
}
|
||||
|
||||
public RxQueryRunner getRxQueryRunner() {
|
||||
return rxQueryRunner;
|
||||
}
|
||||
|
||||
public Publisher<Void> getCommit() {
|
||||
return commit;
|
||||
}
|
||||
|
||||
public Publisher<Void> getRollback() {
|
||||
return rollback;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
|
||||
/**
|
||||
* A delegating mapping function that first calls the delegate to get a record map and than checks the returned
|
||||
* value for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown.
|
||||
* <p>
|
||||
* This class has been introduced instead of {@code Function#andThen} notion to be able throw a decent exception
|
||||
* containing some information about the delegate used and which record was problematic.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @param <T> The expected type of this function
|
||||
* @soundtrack Manowar - Fighting The World
|
||||
* @since 1.0
|
||||
*/
|
||||
class DelegatingMappingFunctionWithNullCheck<T> implements BiFunction<TypeSystem, Record, T> {
|
||||
|
||||
BiFunction<TypeSystem, Record, T> delegate;
|
||||
|
||||
DelegatingMappingFunctionWithNullCheck(BiFunction<TypeSystem, Record, T> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T apply(TypeSystem typeSystem, Record record) {
|
||||
T t = delegate.apply(typeSystem, record);
|
||||
if (t == null) {
|
||||
throw new IllegalStateException(
|
||||
"Mapping function " + delegate + " returned illegal null value for record " + record);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
|
||||
|
||||
/**
|
||||
* Decorator for an ongoing update statement that removes obsolete dynamic labels and adds new ones.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
final class DynamicLabels
|
||||
implements UnaryOperator<OngoingMatchAndUpdate> {
|
||||
|
||||
public static final DynamicLabels EMPTY = new DynamicLabels(Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
private static final Node rootNode = Cypher.anyNode(NAME_OF_ROOT_NODE);
|
||||
|
||||
private final List<String> oldLabels;
|
||||
private final List<String> newLabels;
|
||||
|
||||
DynamicLabels(Collection<String> oldLabels, Collection<String> newLabels) {
|
||||
this.oldLabels = new ArrayList<>(oldLabels);
|
||||
this.newLabels = new ArrayList<>(newLabels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OngoingMatchAndUpdate apply(OngoingMatchAndUpdate ongoingMatchAndUpdate) {
|
||||
|
||||
OngoingMatchAndUpdate decoratedMatchAndUpdate = ongoingMatchAndUpdate;
|
||||
if (!oldLabels.isEmpty()) {
|
||||
decoratedMatchAndUpdate = decoratedMatchAndUpdate.remove(rootNode, oldLabels.toArray(new String[0]));
|
||||
}
|
||||
if (!newLabels.isEmpty()) {
|
||||
decoratedMatchAndUpdate = decoratedMatchAndUpdate.set(rootNode, newLabels.toArray(new String[0]));
|
||||
}
|
||||
return decoratedMatchAndUpdate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Bananafishbones - Viva Conputa
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
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
|
||||
*/
|
||||
void add(String name, 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()
|
||||
));
|
||||
}
|
||||
this.parameters.put(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An unmodifiable copy of this lists 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("%s: %s", e.getKey(), formatValue(e.getValue())))
|
||||
.collect(joining(", ", ":params {", "}"));
|
||||
}
|
||||
|
||||
private static Object formatValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
} 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(joining(", ", "{", "}"));
|
||||
} else if (value instanceof Collection) {
|
||||
return ((Collection) value).stream().map(NamedParameters::formatValue).collect(joining(", ", "[", "]"));
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.QueryRunner;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Definition of a modern Neo4j client.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface Neo4jClient {
|
||||
|
||||
// TODO Create examples how to use the callbacks etc. with Springs TransactionTemplate to deal with rollbacks etc.
|
||||
|
||||
LogAccessor cypherLog = new LogAccessor(LogFactory.getLog("org.neo4j.springframework.data.cypher"));
|
||||
|
||||
static Neo4jClient create(Driver driver) {
|
||||
|
||||
return new DefaultNeo4jClient(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
RunnableSpec 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.
|
||||
*/
|
||||
RunnableSpec 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);
|
||||
|
||||
/**
|
||||
* Contract for a runnable query that can be either run returning it's result, run without results or be parameterized.
|
||||
* @since 1.0
|
||||
*/
|
||||
interface RunnableSpec extends RunnableSpecTightToDatabase {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return A runnable query specification that is now tight to a given database.
|
||||
*/
|
||||
RunnableSpecTightToDatabase in(@Nullable String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for a runnable query inside a dedicated database.
|
||||
* @since 1.0
|
||||
*/
|
||||
interface RunnableSpecTightToDatabase extends BindSpec<RunnableSpecTightToDatabase> {
|
||||
|
||||
/**
|
||||
* 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 binding parameters to a query.
|
||||
*
|
||||
* @param <S> This {@link BindSpec specs} own type
|
||||
* @since 1.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 1.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 1.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 1.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 1.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the delegation in the given target database.
|
||||
*
|
||||
* @param targetDatabase selected database to use
|
||||
* @return An ongoing delegation
|
||||
*/
|
||||
RunnableDelegation<T> in(String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned
|
||||
* @since 1.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.
|
||||
*/
|
||||
static String verifyDatabaseName(String databaseName) {
|
||||
|
||||
String newTargetDatabase = databaseName == null ? null : databaseName.trim();
|
||||
if (newTargetDatabase != null && newTargetDatabase.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Either use null to indicate the default database or a valid database name. The empty string is not permitted.");
|
||||
}
|
||||
return newTargetDatabase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.springframework.data.repository.NoResultException;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
|
||||
/**
|
||||
* 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 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
<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}.
|
||||
*/
|
||||
<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}.
|
||||
*/
|
||||
<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}.
|
||||
*/
|
||||
<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}.
|
||||
*/
|
||||
<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 <T> the type of the entities. Must not be {@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}.
|
||||
*/
|
||||
<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.
|
||||
* @return the loaded entity. Might return an empty optional.
|
||||
*/
|
||||
<T> Optional<T> findById(Object id, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
<T> List<T> findAllById(Iterable<?> ids, 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.
|
||||
*/
|
||||
<T> T save(T instance);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
<T> List<T> saveAll(Iterable<T> instances);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
<T> 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}.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
<T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery);
|
||||
|
||||
/**
|
||||
* An interface for controlling query execution.
|
||||
*
|
||||
* @param <T> the type that gets returned by the query
|
||||
* @since 1.0
|
||||
*/
|
||||
interface ExecutableQuery<T> {
|
||||
|
||||
/**
|
||||
* @return The list of all results. That can be an empty list but is never null.
|
||||
*/
|
||||
List<T> getResults();
|
||||
|
||||
/**
|
||||
* @return An optional, single result.
|
||||
* @throws IncorrectResultSizeDataAccessException when there is more than one result
|
||||
*/
|
||||
Optional<T> getSingleResult();
|
||||
|
||||
/**
|
||||
* @return A required, single result.
|
||||
* @throws NoResultException when there is no result
|
||||
*/
|
||||
T getRequiredSingleResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.neo4j.cypherdsl.core.Cypher.*;
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.exceptions.NoSuchRecordException;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.summary.SummaryCounters;
|
||||
import org.neo4j.cypherdsl.core.Condition;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.renderer.Renderer;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.RunnableSpecTightToDatabase;
|
||||
import org.neo4j.springframework.data.core.NestedRelationshipProcessingStateMachine.ProcessState;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty;
|
||||
import org.neo4j.springframework.data.core.schema.CypherGenerator;
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.neo4j.springframework.data.core.support.Relationships;
|
||||
import org.neo4j.springframework.data.repository.NoResultException;
|
||||
import org.neo4j.springframework.data.repository.event.BeforeBindCallback;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Philipp Tölle
|
||||
* @soundtrack Motörhead - We Are Motörhead
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
|
||||
|
||||
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jTemplate.class));
|
||||
|
||||
private static final String OPTIMISTIC_LOCKING_ERROR_MESSAGE = "An entity with the required version does not exist.";
|
||||
|
||||
private static final Renderer renderer = Renderer.getDefaultRenderer();
|
||||
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
private final Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
private final CypherGenerator cypherGenerator;
|
||||
|
||||
private Neo4jEvents eventSupport;
|
||||
|
||||
private final DatabaseSelectionProvider databaseSelectionProvider;
|
||||
|
||||
public Neo4jTemplate(Neo4jClient neo4jClient) {
|
||||
this(neo4jClient, new Neo4jMappingContext(), DatabaseSelectionProvider.getDefaultSelectionProvider());
|
||||
}
|
||||
|
||||
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, DatabaseSelectionProvider databaseSelectionProvider) {
|
||||
|
||||
Assert.notNull(neo4jClient, "The Neo4jClient is required");
|
||||
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
|
||||
Assert.notNull(databaseSelectionProvider, "The database name provider is required");
|
||||
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.neo4jMappingContext = neo4jMappingContext;
|
||||
this.cypherGenerator = CypherGenerator.INSTANCE;
|
||||
this.eventSupport = new Neo4jEvents(EntityCallbacks.create());
|
||||
|
||||
this.databaseSelectionProvider = databaseSelectionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(Class<?> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
|
||||
.returning(Functions.count(asterisk())).build();
|
||||
|
||||
return count(statement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(Statement statement) {
|
||||
return count(statement, emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(Statement statement, Map<String, Object> parameters) {
|
||||
return count(renderer.render(statement), parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(String cypherQuery) {
|
||||
return count(cypherQuery, emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(String cypherQuery, Map<String, Object> parameters) {
|
||||
|
||||
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class)
|
||||
.withCypherQuery(cypherQuery)
|
||||
.withParameters(parameters)
|
||||
.build();
|
||||
return toExecutableQuery(preparedQuery).getRequiredSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
|
||||
return createExecutableQuery(domainType, statement).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(Statement statement, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, statement).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, statement, parameters).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, statement, parameters).getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(String cypherQuery, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery, parameters).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery, parameters).getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> findById(Object id, Class<T> domainType) {
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator
|
||||
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(NAME_OF_ID)))
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
|
||||
.build();
|
||||
return createExecutableQuery(domainType, statement, singletonMap(NAME_OF_ID, convertIdValues(id))).getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator
|
||||
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(NAME_OF_IDS))))
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
|
||||
.build();
|
||||
|
||||
return createExecutableQuery(domainType, statement, singletonMap(NAME_OF_IDS, convertIdValues(ids))).getResults();
|
||||
}
|
||||
|
||||
private Object convertIdValues(Object idValues) {
|
||||
|
||||
return neo4jMappingContext.getConverter()
|
||||
.writeValueFromProperty(idValues, ClassTypeInformation.from(idValues.getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T save(T instance) {
|
||||
|
||||
return saveImpl(instance, getDatabaseName());
|
||||
}
|
||||
|
||||
private <T> T saveImpl(T instance, @Nullable String inDatabase) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(instance.getClass());
|
||||
T entityToBeSaved = eventSupport.maybeCallBeforeBind(instance);
|
||||
|
||||
DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData, inDatabase);
|
||||
|
||||
Optional<Long> optionalInternalId = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
|
||||
.in(inDatabase)
|
||||
.bind((T) entityToBeSaved)
|
||||
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()))
|
||||
.fetchAs(Long.class).one();
|
||||
|
||||
if (entityMetaData.hasVersionProperty() && !optionalInternalId.isPresent()) {
|
||||
throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
|
||||
if (!entityMetaData.isUsingInternalIds()) {
|
||||
processRelations(entityMetaData, entityToBeSaved, inDatabase);
|
||||
return entityToBeSaved;
|
||||
} else {
|
||||
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), optionalInternalId.get());
|
||||
processRelations(entityMetaData, entityToBeSaved, inDatabase);
|
||||
|
||||
return propertyAccessor.getBean();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> DynamicLabels determineDynamicLabels(
|
||||
T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData, @Nullable String inDatabase
|
||||
) {
|
||||
return entityMetaData.getDynamicLabelsProperty().map(p -> {
|
||||
|
||||
PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
|
||||
RunnableSpecTightToDatabase runnableQuery = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
|
||||
.in(inDatabase)
|
||||
.bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty())).to(NAME_OF_ID)
|
||||
.bind(entityMetaData.getStaticLabels()).to(NAME_OF_STATIC_LABELS_PARAM);
|
||||
|
||||
if (entityMetaData.hasVersionProperty()) {
|
||||
runnableQuery = runnableQuery
|
||||
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
|
||||
.to(NAME_OF_VERSION_PARAM);
|
||||
}
|
||||
|
||||
Optional<Map<String, Object>> optionalResult = runnableQuery.fetch().one();
|
||||
return new DynamicLabels(
|
||||
optionalResult.map(r -> (Collection<String>) r.get(NAME_OF_LABELS)).orElseGet(Collections::emptyList),
|
||||
(Collection<String>) propertyAccessor.getProperty(p)
|
||||
);
|
||||
}).orElse(DynamicLabels.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> saveAll(Iterable<T> instances) {
|
||||
|
||||
String databaseName = getDatabaseName();
|
||||
|
||||
Collection<T> entities;
|
||||
if (instances instanceof Collection) {
|
||||
entities = (Collection<T>) instances;
|
||||
} else {
|
||||
entities = new ArrayList<>();
|
||||
instances.forEach(entities::add);
|
||||
}
|
||||
|
||||
if (entities.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Class<T> domainClass = (Class<T>) CollectionUtils.findCommonElementType(entities);
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainClass);
|
||||
if (entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()) {
|
||||
log.debug("Saving entities using single statements.");
|
||||
|
||||
return entities.stream()
|
||||
.map(e -> saveImpl(e, databaseName))
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
List<T> entitiesToBeSaved = entities.stream()
|
||||
.map(eventSupport::maybeCallBeforeBind)
|
||||
.collect(toList());
|
||||
|
||||
// Save roots
|
||||
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
|
||||
List<Map<String, Object>> entityList = entitiesToBeSaved.stream()
|
||||
.map(binderFunction).collect(toList());
|
||||
ResultSummary resultSummary = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
|
||||
.in(databaseName)
|
||||
.bind(entityList).to(NAME_OF_ENTITY_LIST_PARAM)
|
||||
.run();
|
||||
|
||||
// Save related
|
||||
entitiesToBeSaved.forEach(entityToBeSaved -> processRelations(entityMetaData, entityToBeSaved, databaseName));
|
||||
|
||||
SummaryCounters counters = resultSummary.counters();
|
||||
log.debug(() -> String
|
||||
.format("Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
|
||||
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
|
||||
counters.relationshipsDeleted(), counters.propertiesSet()));
|
||||
|
||||
return entitiesToBeSaved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void deleteById(Object id, Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
String nameOfParameter = "id";
|
||||
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
|
||||
|
||||
log.debug(() -> String.format("Deleting entity with id %s ", id));
|
||||
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
|
||||
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
|
||||
.in(getDatabaseName())
|
||||
.bind(id).to(nameOfParameter)
|
||||
.run();
|
||||
|
||||
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
|
||||
summary.counters().relationshipsDeleted()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void deleteAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
String nameOfParameter = "ids";
|
||||
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
|
||||
|
||||
log.debug(() -> String.format("Deleting all entities with the following ids: %s ", ids));
|
||||
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
|
||||
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
|
||||
.in(getDatabaseName())
|
||||
.bind(ids).to(nameOfParameter)
|
||||
.run();
|
||||
|
||||
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
|
||||
summary.counters().relationshipsDeleted()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(Class<?> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
log.debug(() -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel()));
|
||||
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData);
|
||||
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).run();
|
||||
|
||||
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
|
||||
summary.counters().relationshipsDeleted()));
|
||||
}
|
||||
|
||||
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, Statement statement) {
|
||||
return createExecutableQuery(domainType, statement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, String cypherStatement) {
|
||||
return createExecutableQuery(domainType, cypherStatement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, Statement statement,
|
||||
Map<String, Object> parameters) {
|
||||
|
||||
return createExecutableQuery(domainType, renderer.render(statement), parameters);
|
||||
}
|
||||
|
||||
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, String cypherStatement,
|
||||
Map<String, Object> parameters) {
|
||||
|
||||
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType)
|
||||
.withCypherQuery(cypherStatement)
|
||||
.withParameters(parameters)
|
||||
.usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType))
|
||||
.build();
|
||||
return toExecutableQuery(preparedQuery);
|
||||
}
|
||||
|
||||
private void processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
|
||||
@Nullable String inDatabase) {
|
||||
|
||||
processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine());
|
||||
}
|
||||
|
||||
private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
|
||||
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
|
||||
|
||||
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
|
||||
Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty());
|
||||
|
||||
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
|
||||
|
||||
// create context to bundle parameters
|
||||
NestedRelationshipContext relationshipContext = NestedRelationshipContext
|
||||
.of(association, propertyAccessor, neo4jPersistentEntity);
|
||||
|
||||
Collection<?> relatedValuesToStore = Relationships
|
||||
.unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue());
|
||||
|
||||
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
|
||||
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
|
||||
|
||||
// break recursive procession and deletion of previously created relationships
|
||||
ProcessState processState = stateMachine
|
||||
.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
|
||||
if (processState == ProcessState.PROCESSED_BOTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all relationships before creating all new if the entity is not new
|
||||
// this avoids the usage of cache but might have significant impact on overall performance
|
||||
if (!neo4jPersistentEntity.isNew(parentObject)) {
|
||||
Neo4jPersistentEntity<?> previouslyRelatedPersistentEntity = neo4jMappingContext
|
||||
.getPersistentEntity(relationshipContext.getAssociationTargetType());
|
||||
|
||||
Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity,
|
||||
relationshipDescription, previouslyRelatedPersistentEntity);
|
||||
|
||||
neo4jClient.query(renderer.render(relationshipRemoveQuery))
|
||||
.in(inDatabase)
|
||||
.bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME).run();
|
||||
}
|
||||
|
||||
// nothing to do because there is nothing to map
|
||||
if (relationshipContext.inverseValueIsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateMachine.markAsProcessed(relationshipDescription, relatedValuesToStore);
|
||||
|
||||
for (Object relatedValueToStore : relatedValuesToStore) {
|
||||
|
||||
// here map entry is not always anymore a dynamic association
|
||||
Object valueToBeSavedPreEvt = relationshipContext
|
||||
.identifyAndExtractRelationshipValue(relatedValueToStore);
|
||||
valueToBeSavedPreEvt = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt);
|
||||
|
||||
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
|
||||
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
|
||||
|
||||
Long relatedInternalId = saveRelatedNode(valueToBeSavedPreEvt,
|
||||
relationshipContext.getAssociationTargetType(),
|
||||
targetNodeDescription, inDatabase);
|
||||
|
||||
RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement(
|
||||
neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId,
|
||||
relatedValueToStore);
|
||||
|
||||
neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery()))
|
||||
.in(inDatabase)
|
||||
.bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME)
|
||||
.bindAll(statementHolder.getProperties())
|
||||
.run();
|
||||
|
||||
// if an internal id is used this must get set to link this entity in the next iteration
|
||||
if (targetNodeDescription.isUsingInternalIds()) {
|
||||
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
|
||||
.getPropertyAccessor(valueToBeSavedPreEvt);
|
||||
targetPropertyAccessor
|
||||
.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
|
||||
}
|
||||
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
|
||||
processNestedRelations(targetNodeDescription, valueToBeSavedPreEvt, inDatabase, stateMachine);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <Y> Long saveRelatedNode(Object entity, Class<Y> entityType, NodeDescription targetNodeDescription, @Nullable String inDatabase) {
|
||||
|
||||
DynamicLabels dynamicLabels = determineDynamicLabels(entity, (Neo4jPersistentEntity) targetNodeDescription, inDatabase);
|
||||
Optional<Long> optionalSavedNodeId = neo4jClient
|
||||
.query(() -> renderer
|
||||
.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
|
||||
.in(inDatabase)
|
||||
.bind((Y) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
|
||||
.fetchAs(Long.class).one();
|
||||
|
||||
if (((Neo4jPersistentEntity) targetNodeDescription).hasVersionProperty() && !optionalSavedNodeId.isPresent()) {
|
||||
throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
return optionalSavedNodeId.get();
|
||||
}
|
||||
|
||||
private String getDatabaseName() {
|
||||
|
||||
return this.databaseSelectionProvider.getDatabaseSelection().getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
|
||||
this.eventSupport = new Neo4jEvents(EntityCallbacks.create(beanFactory));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery) {
|
||||
|
||||
Neo4jClient.MappingSpec<T> mappingSpec = this
|
||||
.neo4jClient.query(preparedQuery.getCypherQuery())
|
||||
.in(getDatabaseName())
|
||||
.bindAll(preparedQuery.getParameters())
|
||||
.fetchAs(preparedQuery.getResultType());
|
||||
Neo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery
|
||||
.getOptionalMappingFunction()
|
||||
.map(f -> mappingSpec.mappedBy(f))
|
||||
.orElse(mappingSpec);
|
||||
|
||||
return new DefaultExecutableQuery<>(preparedQuery, fetchSpec);
|
||||
}
|
||||
|
||||
final class DefaultExecutableQuery<T> implements ExecutableQuery<T> {
|
||||
|
||||
private final PreparedQuery<T> preparedQuery;
|
||||
private final Neo4jClient.RecordFetchSpec<T> fetchSpec;
|
||||
|
||||
DefaultExecutableQuery(PreparedQuery<T> preparedQuery, Neo4jClient.RecordFetchSpec<T> fetchSpec) {
|
||||
this.preparedQuery = preparedQuery;
|
||||
this.fetchSpec = fetchSpec;
|
||||
}
|
||||
|
||||
public List<T> getResults() {
|
||||
return fetchSpec.all().stream().collect(toList());
|
||||
}
|
||||
|
||||
public Optional<T> getSingleResult() {
|
||||
try {
|
||||
return fetchSpec.one();
|
||||
} catch (NoSuchRecordException e) {
|
||||
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
|
||||
// So there has been an incorrect result size, but not to few results but to many.
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
}
|
||||
|
||||
public T getRequiredSingleResult() {
|
||||
return fetchSpec.one()
|
||||
.orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class that orchestrates {@link EntityCallbacks}.
|
||||
* All the methods provided here check for their availability and do nothing when an event cannot be published.
|
||||
*/
|
||||
final class Neo4jEvents {
|
||||
|
||||
private final EntityCallbacks entityCallbacks;
|
||||
|
||||
Neo4jEvents(EntityCallbacks entityCallbacks) {
|
||||
this.entityCallbacks = entityCallbacks;
|
||||
}
|
||||
|
||||
public <T> T maybeCallBeforeBind(T object) {
|
||||
return entityCallbacks.callback(BeforeBindCallback.class, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Working on nested relationships happens in a certain algorithmic context.
|
||||
* This context enables a tight cohesion between the algorithmic steps and the data, these steps are performed on.
|
||||
* In our the interaction happens between the data that describes the relationship and the specific steps of
|
||||
* the algorithm.
|
||||
*
|
||||
* @author Philipp Tölle
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
final class NestedRelationshipContext {
|
||||
private final Neo4jPersistentProperty inverse;
|
||||
private final Object value;
|
||||
private final RelationshipDescription relationship;
|
||||
private final Class<?> associationTargetType;
|
||||
|
||||
private final boolean inverseValueIsEmpty;
|
||||
|
||||
private NestedRelationshipContext(Neo4jPersistentProperty inverse, @Nullable Object value,
|
||||
RelationshipDescription relationship, Class<?> associationTargetType, boolean inverseValueIsEmpty) {
|
||||
this.inverse = inverse;
|
||||
this.value = value;
|
||||
this.relationship = relationship;
|
||||
this.associationTargetType = associationTargetType;
|
||||
this.inverseValueIsEmpty = inverseValueIsEmpty;
|
||||
}
|
||||
|
||||
Neo4jPersistentProperty getInverse() {
|
||||
return inverse;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
RelationshipDescription getRelationship() {
|
||||
return relationship;
|
||||
}
|
||||
|
||||
Class<?> getAssociationTargetType() {
|
||||
return associationTargetType;
|
||||
}
|
||||
|
||||
public boolean inverseValueIsEmpty() {
|
||||
return inverseValueIsEmpty;
|
||||
}
|
||||
|
||||
boolean hasRelationshipWithProperties() {
|
||||
return this.relationship.hasRelationshipProperties();
|
||||
}
|
||||
|
||||
Object identifyAndExtractRelationshipValue(Object relatedValue) {
|
||||
Object valueToBeSaved = relatedValue;
|
||||
if (relatedValue instanceof Map.Entry) {
|
||||
Map.Entry relatedValueMapEntry = (Map.Entry) relatedValue;
|
||||
|
||||
if (this.getInverse().isDynamicAssociation()) {
|
||||
valueToBeSaved = relatedValueMapEntry.getValue();
|
||||
} else if (this.hasRelationshipWithProperties()) {
|
||||
valueToBeSaved = relatedValueMapEntry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
return valueToBeSaved;
|
||||
}
|
||||
|
||||
static NestedRelationshipContext of(Association<Neo4jPersistentProperty> handler,
|
||||
PersistentPropertyAccessor<?> propertyAccessor,
|
||||
Neo4jPersistentEntity<?> neo4jPersistentEntity) {
|
||||
|
||||
Neo4jPersistentProperty inverse = handler.getInverse();
|
||||
|
||||
boolean inverseValueIsEmpty = propertyAccessor.getProperty(inverse) == null;
|
||||
Object value = propertyAccessor.getProperty(inverse);
|
||||
|
||||
RelationshipDescription relationship = neo4jPersistentEntity
|
||||
.getRelationships().stream()
|
||||
.filter(r -> r.getFieldName().equals(inverse.getName()))
|
||||
.findFirst().get();
|
||||
|
||||
// if we have a relationship with properties, the targetNodeType is the map key
|
||||
Class<?> associationTargetType = relationship.hasRelationshipProperties()
|
||||
? inverse.getComponentType()
|
||||
: inverse.getAssociationTargetType();
|
||||
|
||||
return new NestedRelationshipContext(inverse, value, relationship, associationTargetType,
|
||||
inverseValueIsEmpty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* This stores all processed nested relations and objects during save of objects so that the recursive descent can be
|
||||
* stopped accordingly.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Helge Schneider - Heart Attack No. 1
|
||||
*/
|
||||
final class NestedRelationshipProcessingStateMachine {
|
||||
|
||||
enum ProcessState {
|
||||
PROCESSED_NONE,
|
||||
PROCESSED_BOTH,
|
||||
PROCESSED_ONLY_RELATIONSHIP,
|
||||
PROCESSED_ALL_VALUES
|
||||
}
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final Lock read = lock.readLock();
|
||||
private final Lock write = lock.writeLock();
|
||||
|
||||
/**
|
||||
* The set of already processed relationships.
|
||||
*/
|
||||
private final Set<RelationshipDescription> processedRelationshipDescriptions = new HashSet<>();
|
||||
|
||||
/**
|
||||
* The set of already processed related objects.
|
||||
*/
|
||||
private final Set<Object> processedObjects = new HashSet<>();
|
||||
|
||||
/**
|
||||
* @param relationshipDescription Check whether this relationship description has been processed
|
||||
* @param valuesToStore Check whether all the values in the collection have been processed
|
||||
* @return The state of things processed
|
||||
*/
|
||||
ProcessState getStateOf(RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
|
||||
|
||||
try {
|
||||
read.lock();
|
||||
boolean hasProcessedRelationship = hasProcessed(relationshipDescription);
|
||||
boolean hasProcessedAllValues = hasProcessedAllOf(valuesToStore);
|
||||
if (hasProcessedRelationship && hasProcessedAllValues) {
|
||||
return ProcessState.PROCESSED_BOTH;
|
||||
}
|
||||
if (hasProcessedRelationship) {
|
||||
return ProcessState.PROCESSED_ONLY_RELATIONSHIP;
|
||||
}
|
||||
if (hasProcessedAllValues) {
|
||||
return ProcessState.PROCESSED_ALL_VALUES;
|
||||
}
|
||||
return ProcessState.PROCESSED_NONE;
|
||||
} finally {
|
||||
read.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the passed objects as processed
|
||||
*
|
||||
* @param relationshipDescription To be marked as processed
|
||||
* @param valuesToStore If not {@literal null}, all non-null values will be marked as processed
|
||||
*/
|
||||
void markAsProcessed(RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
|
||||
|
||||
try {
|
||||
write.lock();
|
||||
this.processedRelationshipDescriptions.add(relationshipDescription);
|
||||
if (valuesToStore != null) {
|
||||
valuesToStore.stream().filter(v -> v != null).forEach(processedObjects::add);
|
||||
}
|
||||
} finally {
|
||||
write.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasProcessedAllOf(@Nullable Collection<?> valuesToStore) {
|
||||
// there can be null elements in the unified collection of values to store.
|
||||
if (valuesToStore == null) {
|
||||
return false;
|
||||
}
|
||||
return processedObjects.containsAll(valuesToStore);
|
||||
}
|
||||
|
||||
private boolean hasProcessed(RelationshipDescription relationshipDescription) {
|
||||
|
||||
if (relationshipDescription != null) {
|
||||
return processedRelationshipDescriptions.contains(relationshipDescription);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param <T> The type of the objects returned by this query.
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Deichkind - Arbeit nervt
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class PreparedQuery<T> {
|
||||
|
||||
public static <CT> RequiredBuildStep<CT> queryFor(Class<CT> resultType) {
|
||||
return new RequiredBuildStep<CT>(resultType);
|
||||
}
|
||||
|
||||
private final Class<T> resultType;
|
||||
private final String cypherQuery;
|
||||
private final Map<String, Object> parameters;
|
||||
private final @Nullable BiFunction<TypeSystem, Record, T> mappingFunction;
|
||||
|
||||
private PreparedQuery(OptionalBuildSteps<T> optionalBuildSteps) {
|
||||
this.resultType = optionalBuildSteps.resultType;
|
||||
this.mappingFunction = (BiFunction<TypeSystem, Record, T>) optionalBuildSteps.mappingFunction;
|
||||
this.cypherQuery = optionalBuildSteps.cypherQuery;
|
||||
this.parameters = optionalBuildSteps.parameters;
|
||||
}
|
||||
|
||||
public Class<T> getResultType() {
|
||||
return this.resultType;
|
||||
}
|
||||
|
||||
public Optional<BiFunction<TypeSystem, Record, T>> getOptionalMappingFunction() {
|
||||
return Optional.ofNullable(mappingFunction);
|
||||
}
|
||||
|
||||
public String getCypherQuery() {
|
||||
return this.cypherQuery;
|
||||
}
|
||||
|
||||
public Map<String, Object> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <CT> The concrete type of this build step.
|
||||
* @since 1.0
|
||||
*/
|
||||
public static class RequiredBuildStep<CT> {
|
||||
private final Class<CT> resultType;
|
||||
|
||||
private RequiredBuildStep(Class<CT> resultType) {
|
||||
this.resultType = resultType;
|
||||
}
|
||||
|
||||
public OptionalBuildSteps<CT> withCypherQuery(String cypherQuery) {
|
||||
return new OptionalBuildSteps<>(resultType, cypherQuery);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <CT> The concrete type of this build step.
|
||||
* @since 1.0
|
||||
*/
|
||||
public static class OptionalBuildSteps<CT> {
|
||||
|
||||
final Class<CT> resultType;
|
||||
final String cypherQuery;
|
||||
Map<String, Object> parameters = Collections.emptyMap();
|
||||
@Nullable BiFunction<TypeSystem, Record, ?> mappingFunction;
|
||||
|
||||
OptionalBuildSteps(Class<CT> resultType, String cypherQuery) {
|
||||
this.resultType = resultType;
|
||||
this.cypherQuery = cypherQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* This replaces the current parameters.
|
||||
*
|
||||
* @param newParameters The new parameters for the prepared query.
|
||||
* @return This builder.
|
||||
*/
|
||||
public OptionalBuildSteps<CT> withParameters(Map<String, Object> newParameters) {
|
||||
this.parameters = new HashMap<>(newParameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OptionalBuildSteps<CT> usingMappingFunction(
|
||||
@Nullable BiFunction<TypeSystem, Record, ?> newMappingFunction) {
|
||||
this.mappingFunction = newMappingFunction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreparedQuery<CT> build() {
|
||||
return new PreparedQuery<>(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
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.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Rage - Reign Of Fear
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@FunctionalInterface
|
||||
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.
|
||||
*/
|
||||
static ReactiveDatabaseSelectionProvider createStaticDatabaseSelectionProvider(String databaseName) {
|
||||
|
||||
Assert.notNull(databaseName, "The database name must not be null.");
|
||||
Assert.hasText(databaseName, "The database name must not be empty.");
|
||||
|
||||
return () -> Mono.just(DatabaseSelection.byName(databaseName));
|
||||
}
|
||||
|
||||
/**
|
||||
* A database selector always selecting the default database.
|
||||
*
|
||||
* @return A provider for the default database name.
|
||||
*/
|
||||
static ReactiveDatabaseSelectionProvider getDefaultSelectionProvider() {
|
||||
|
||||
return DefaultReactiveDatabaseSelectionProvider.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
enum DefaultReactiveDatabaseSelectionProvider implements ReactiveDatabaseSelectionProvider {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Mono<DatabaseSelection> getDatabaseSelection() {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.reactive.RxQueryRunner;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.BindSpec;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
|
||||
/**
|
||||
* 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 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface ReactiveNeo4jClient {
|
||||
|
||||
LogAccessor cypherLog = new LogAccessor(LogFactory.getLog("org.neo4j.springframework.data.cypher"));
|
||||
|
||||
static ReactiveNeo4jClient create(Driver driver) {
|
||||
|
||||
return new DefaultReactiveNeo4jClient(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
RunnableSpec 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.
|
||||
*/
|
||||
RunnableSpec 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<RxQueryRunner, Mono<T>> callback);
|
||||
|
||||
/**
|
||||
* @param <T> The resulting type of this mapping
|
||||
* @since 1.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 1.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 it's result, run without results or be parameterized.
|
||||
* @since 1.0
|
||||
*/
|
||||
interface RunnableSpec extends RunnableSpecTightToDatabase {
|
||||
|
||||
/**
|
||||
* Pins the previously defined query to a specific database.
|
||||
*
|
||||
* @param targetDatabase selected database to use
|
||||
* @return A runnable query specification that is now tight to a given database.
|
||||
*/
|
||||
RunnableSpecTightToDatabase in(String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for a runnable query inside a dedicated database.
|
||||
* @since 1.0
|
||||
*/
|
||||
interface RunnableSpecTightToDatabase extends BindSpec<RunnableSpecTightToDatabase> {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* A contract for an ongoing delegation in the selected database.
|
||||
*
|
||||
* @param <T> The type of the returned value.
|
||||
* @since 1.0
|
||||
*/
|
||||
interface OngoingDelegation<T> extends RunnableDelegation<T> {
|
||||
|
||||
/**
|
||||
* Runs the delegation in the given target database.
|
||||
*
|
||||
* @param targetDatabase selected database to use
|
||||
* @return An ongoing delegation
|
||||
*/
|
||||
RunnableDelegation<T> in(String targetDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable delegation.
|
||||
*
|
||||
* @param <T> the type that gets returned by the query
|
||||
* @since 1.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
|
||||
/**
|
||||
* Specifies reactive operations one can perform on a database, based on an <em>Domain Type</em>.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
<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 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> 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 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> 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 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> 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 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> 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 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> 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 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> 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.
|
||||
* @return the loaded entity. Might return an empty optional.
|
||||
*/
|
||||
<T> Mono<T> findById(Object id, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
<T> Flux<T> findAllById(Iterable<?> ids, 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.
|
||||
*/
|
||||
<T> Mono<T> save(T instance);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
<T> Flux<T> saveAll(Iterable<T> instances);
|
||||
|
||||
/**
|
||||
* 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> Mono<Void> deleteById(Object id, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
<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}.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
<T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> preparedQuery);
|
||||
|
||||
/**
|
||||
* An interface for controlling query execution in a reactive fashion.
|
||||
*
|
||||
* @param <T> the type that gets returned by the query
|
||||
* @since 1.0
|
||||
*/
|
||||
interface ExecutableQuery<T> {
|
||||
|
||||
/**
|
||||
* @return All results returned by this query.
|
||||
*/
|
||||
Flux<T> getResults();
|
||||
|
||||
/**
|
||||
* @return A single result
|
||||
* @throws IncorrectResultSizeDataAccessException if there are more than one result
|
||||
*/
|
||||
Mono<T> getSingleResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.neo4j.cypherdsl.core.Cypher.*;
|
||||
import static org.neo4j.springframework.data.core.DatabaseSelection.*;
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.exceptions.NoSuchRecordException;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.summary.SummaryCounters;
|
||||
import org.neo4j.cypherdsl.core.Condition;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.renderer.Renderer;
|
||||
import org.neo4j.springframework.data.core.NestedRelationshipProcessingStateMachine.ProcessState;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty;
|
||||
import org.neo4j.springframework.data.core.schema.CypherGenerator;
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.neo4j.springframework.data.core.support.Relationships;
|
||||
import org.neo4j.springframework.data.repository.event.ReactiveBeforeBindCallback;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @author Philipp Tölle
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, BeanFactoryAware {
|
||||
|
||||
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(ReactiveNeo4jTemplate.class));
|
||||
|
||||
private static final String OPTIMISTIC_LOCKING_ERROR_MESSAGE = "An entity with the required version does not exist.";
|
||||
|
||||
private static final Renderer renderer = Renderer.getDefaultRenderer();
|
||||
|
||||
private final ReactiveNeo4jClient neo4jClient;
|
||||
|
||||
private final Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
private final CypherGenerator cypherGenerator;
|
||||
|
||||
private ReactiveNeo4jEvents eventSupport;
|
||||
|
||||
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
|
||||
|
||||
public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext,
|
||||
ReactiveDatabaseSelectionProvider databaseSelectionProvider) {
|
||||
|
||||
Assert.notNull(neo4jClient, "The Neo4jClient is required");
|
||||
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
|
||||
Assert.notNull(databaseSelectionProvider, "The database selection provider is required");
|
||||
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.neo4jMappingContext = neo4jMappingContext;
|
||||
this.cypherGenerator = CypherGenerator.INSTANCE;
|
||||
this.eventSupport = new ReactiveNeo4jEvents(ReactiveEntityCallbacks.create());
|
||||
this.databaseSelectionProvider = databaseSelectionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(Class<?> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
|
||||
.returning(Functions.count(asterisk())).build();
|
||||
|
||||
return count(statement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(Statement statement) {
|
||||
return count(statement, emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(Statement statement, Map<String, Object> parameters) {
|
||||
return count(renderer.render(statement), parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(String cypherQuery) {
|
||||
return count(cypherQuery, emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(String cypherQuery, Map<String, Object> parameters) {
|
||||
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class)
|
||||
.withCypherQuery(cypherQuery)
|
||||
.withParameters(parameters)
|
||||
.build();
|
||||
return this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> findAll(Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
|
||||
return createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> findAll(Statement statement, Class<T> domainType) {
|
||||
|
||||
return createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
@Override public <T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
|
||||
|
||||
return createExecutableQuery(domainType, statement, parameters).flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
|
||||
|
||||
return createExecutableQuery(domainType, statement, parameters).flatMap(ExecutableQuery::getSingleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> findAll(String cypherQuery, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery).flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
|
||||
return createExecutableQuery(domainType, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> findById(Object id, Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator
|
||||
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(NAME_OF_ID)))
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
|
||||
.build();
|
||||
|
||||
return createExecutableQuery(domainType, statement, singletonMap(NAME_OF_ID, convertIdValues(id)))
|
||||
.flatMap(ExecutableQuery::getSingleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> findAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator
|
||||
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(NAME_OF_IDS))))
|
||||
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
|
||||
.build();
|
||||
|
||||
return createExecutableQuery(domainType, statement, singletonMap(NAME_OF_IDS, convertIdValues(ids)))
|
||||
.flatMapMany(ExecutableQuery::getResults);
|
||||
}
|
||||
|
||||
private Object convertIdValues(Object idValues) {
|
||||
|
||||
return neo4jMappingContext.getConverter()
|
||||
.writeValueFromProperty(idValues, ClassTypeInformation.from(idValues.getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> save(T instance) {
|
||||
|
||||
return getDatabaseName().flatMap(databaseName -> saveImpl(instance, databaseName.getValue()));
|
||||
}
|
||||
|
||||
private <T> Mono<T> saveImpl(T instance, @Nullable String inDatabase) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(instance.getClass());
|
||||
return Mono.just(instance)
|
||||
.flatMap(eventSupport::maybeCallBeforeBind)
|
||||
.flatMap(entity -> determineDynamicLabels(entity, entityMetaData, inDatabase))
|
||||
.flatMap(t -> {
|
||||
T entity = t.getT1();
|
||||
DynamicLabels dynamicLabels = t.getT2();
|
||||
|
||||
Statement saveStatement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels);
|
||||
|
||||
Mono<Long> idMono =
|
||||
this.neo4jClient.query(() -> renderer.render(saveStatement))
|
||||
.in(inDatabase)
|
||||
.bind((T) entity)
|
||||
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entity.getClass()))
|
||||
.fetchAs(Long.class).one()
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
if (entityMetaData.hasVersionProperty()) {
|
||||
return Mono.error(
|
||||
() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
|
||||
}
|
||||
return Mono.empty();
|
||||
}));
|
||||
|
||||
|
||||
if (!entityMetaData.isUsingInternalIds()) {
|
||||
return idMono.then(processRelations(entityMetaData, entity, inDatabase)).thenReturn(entity);
|
||||
} else {
|
||||
return idMono.map(internalId -> {
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entity);
|
||||
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
|
||||
|
||||
return propertyAccessor.getBean();
|
||||
}).flatMap(savedEntity -> processRelations(entityMetaData, savedEntity, inDatabase)
|
||||
.thenReturn(savedEntity));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(
|
||||
T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData, @Nullable String inDatabase
|
||||
) {
|
||||
return entityMetaData.getDynamicLabelsProperty().map(p -> {
|
||||
|
||||
PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
|
||||
ReactiveNeo4jClient.RunnableSpecTightToDatabase runnableQuery = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
|
||||
.in(inDatabase)
|
||||
.bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty())).to(NAME_OF_ID)
|
||||
.bind(entityMetaData.getStaticLabels()).to(NAME_OF_STATIC_LABELS_PARAM);
|
||||
|
||||
if (entityMetaData.hasVersionProperty()) {
|
||||
runnableQuery = runnableQuery
|
||||
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
|
||||
.to(NAME_OF_VERSION_PARAM);
|
||||
}
|
||||
|
||||
return runnableQuery.fetch().one()
|
||||
.map(m -> (Collection<String>) m.get(NAME_OF_LABELS))
|
||||
.switchIfEmpty(Mono.just(Collections.emptyList()))
|
||||
.zipWith(Mono.just((Collection<String>) propertyAccessor.getProperty(p)))
|
||||
.map(t -> Tuples.of(entityToBeSaved, new DynamicLabels(t.getT1(), t.getT2())));
|
||||
}).orElse(Mono.just(Tuples.of(entityToBeSaved, DynamicLabels.EMPTY)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> saveAll(Iterable<T> instances) {
|
||||
|
||||
Collection<T> entities;
|
||||
if (instances instanceof Collection) {
|
||||
entities = (Collection<T>) instances;
|
||||
} else {
|
||||
entities = new ArrayList<>();
|
||||
instances.forEach(entities::add);
|
||||
}
|
||||
|
||||
if (entities.isEmpty()) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
Class<T> domainClass = (Class<T>) CollectionUtils.findCommonElementType(entities);
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainClass);
|
||||
|
||||
if (entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()) {
|
||||
log.debug("Saving entities using single statements.");
|
||||
|
||||
return getDatabaseName().flatMapMany(databaseName ->
|
||||
Flux.fromIterable(entities).flatMap(e -> this.saveImpl(e, databaseName.getValue())));
|
||||
}
|
||||
|
||||
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
|
||||
return getDatabaseName().flatMapMany(databaseName ->
|
||||
Flux.fromIterable(entities)
|
||||
.flatMap(eventSupport::maybeCallBeforeBind)
|
||||
.collectList()
|
||||
.flatMapMany(
|
||||
entitiesToBeSaved -> Mono
|
||||
.defer(() -> { // Defer the actual save statement until the previous flux completes
|
||||
List<Map<String, Object>> boundedEntityList = entitiesToBeSaved.stream()
|
||||
.map(binderFunction)
|
||||
.collect(toList());
|
||||
|
||||
return neo4jClient
|
||||
.query(() -> renderer
|
||||
.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
|
||||
.in(databaseName.getValue())
|
||||
.bind(boundedEntityList).to(NAME_OF_ENTITY_LIST_PARAM).run();
|
||||
})
|
||||
.doOnNext(resultSummary -> {
|
||||
SummaryCounters counters = resultSummary.counters();
|
||||
log.debug(() -> String.format(
|
||||
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
|
||||
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
|
||||
counters.relationshipsDeleted(), counters.propertiesSet()));
|
||||
})
|
||||
.thenMany(Flux.fromIterable(entitiesToBeSaved))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<Void> deleteAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
String nameOfParameter = "ids";
|
||||
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
|
||||
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
|
||||
return getDatabaseName().flatMap(databaseName ->
|
||||
this.neo4jClient.query(() -> renderer.render(statement))
|
||||
.in(databaseName.getValue())
|
||||
.bind(ids).to(nameOfParameter).run().then());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<Void> deleteById(Object id, Class<T> domainType) {
|
||||
|
||||
Assert.notNull(id, "The given id must not be null!");
|
||||
|
||||
String nameOfParameter = "id";
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
|
||||
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
|
||||
return getDatabaseName().flatMap(databaseName ->
|
||||
this.neo4jClient.query(() -> renderer.render(statement))
|
||||
.in(databaseName.getValue())
|
||||
.bind(id).to(nameOfParameter).run().then());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteAll(Class<?> domainType) {
|
||||
|
||||
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData);
|
||||
return getDatabaseName().flatMap(databaseName ->
|
||||
this.neo4jClient.query(() -> renderer.render(statement))
|
||||
.in(databaseName.getValue()).run().then());
|
||||
}
|
||||
|
||||
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, Statement statement) {
|
||||
return createExecutableQuery(domainType, statement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, String cypherQuery) {
|
||||
return createExecutableQuery(domainType, cypherQuery, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, Statement statement,
|
||||
Map<String, Object> parameters) {
|
||||
|
||||
return createExecutableQuery(domainType, renderer.render(statement), parameters);
|
||||
}
|
||||
|
||||
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, String cypherQuery,
|
||||
Map<String, Object> parameters) {
|
||||
|
||||
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType)
|
||||
.withCypherQuery(cypherQuery)
|
||||
.withParameters(parameters)
|
||||
.usingMappingFunction(this.neo4jMappingContext.getRequiredMappingFunctionFor(domainType))
|
||||
.build();
|
||||
return this.toExecutableQuery(preparedQuery);
|
||||
}
|
||||
|
||||
private Mono<Void> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) {
|
||||
|
||||
return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine());
|
||||
}
|
||||
|
||||
private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
|
||||
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
|
||||
|
||||
return Mono.defer(() -> {
|
||||
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
|
||||
Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty());
|
||||
List<Mono<Void>> relationshipCreationMonos = new ArrayList<>();
|
||||
|
||||
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
|
||||
|
||||
// create context to bundle parameters
|
||||
NestedRelationshipContext relationshipContext = NestedRelationshipContext
|
||||
.of(association, propertyAccessor, neo4jPersistentEntity);
|
||||
|
||||
Collection<?> relatedValuesToStore = Relationships
|
||||
.unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue());
|
||||
|
||||
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
|
||||
RelationshipDescription relationshipDescriptionObverse = relationshipDescription
|
||||
.getRelationshipObverse();
|
||||
|
||||
// break recursive procession and deletion of previously created relationships
|
||||
ProcessState processState = stateMachine
|
||||
.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
|
||||
if (processState == ProcessState.PROCESSED_BOTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all relationships before creating all new if the entity is not new
|
||||
// this avoids the usage of cache but might have significant impact on overall performance
|
||||
if (!neo4jPersistentEntity.isNew(parentObject)) {
|
||||
Neo4jPersistentEntity<?> previouslyRelatedPersistentEntity = neo4jMappingContext
|
||||
.getPersistentEntity(relationshipContext.getAssociationTargetType());
|
||||
|
||||
Statement relationshipRemoveQuery = cypherGenerator
|
||||
.createRelationshipRemoveQuery(neo4jPersistentEntity, relationshipDescription,
|
||||
previouslyRelatedPersistentEntity);
|
||||
relationshipCreationMonos.add(
|
||||
neo4jClient.query(renderer.render(relationshipRemoveQuery))
|
||||
.in(inDatabase)
|
||||
.bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME)
|
||||
.run().checkpoint("delete relationships").then());
|
||||
}
|
||||
|
||||
// nothing to do because there is nothing to map
|
||||
if (relationshipContext.inverseValueIsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateMachine.markAsProcessed(relationshipDescription, relatedValuesToStore);
|
||||
|
||||
for (Object relatedValueToStore : relatedValuesToStore) {
|
||||
|
||||
Object valueToBeSavedPreEvt = relationshipContext
|
||||
.identifyAndExtractRelationshipValue(relatedValueToStore);
|
||||
|
||||
Mono<Void> createRelationship = eventSupport
|
||||
.maybeCallBeforeBind(valueToBeSavedPreEvt)
|
||||
.flatMap(valueToBeSaved -> {
|
||||
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
|
||||
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
|
||||
return saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
|
||||
targetNodeDescription, inDatabase)
|
||||
.flatMap(relatedInternalId -> {
|
||||
|
||||
// if an internal id is used this must get set to link this entity in the next iteration
|
||||
if (targetNodeDescription.isUsingInternalIds()) {
|
||||
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
|
||||
.getPropertyAccessor(valueToBeSaved);
|
||||
targetPropertyAccessor
|
||||
.setProperty(targetNodeDescription.getRequiredIdProperty(),
|
||||
relatedInternalId);
|
||||
}
|
||||
|
||||
RelationshipStatementHolder statementHolder = RelationshipStatementHolder
|
||||
.createStatement(
|
||||
neo4jMappingContext, neo4jPersistentEntity, relationshipContext,
|
||||
relatedInternalId, relatedValueToStore);
|
||||
|
||||
// in case of no properties the bind will just return an empty map
|
||||
Mono<ResultSummary> relationshipCreationMonoNested = neo4jClient
|
||||
.query(renderer.render(statementHolder.getRelationshipCreationQuery()))
|
||||
.in(inDatabase)
|
||||
.bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME)
|
||||
.bindAll(statementHolder.getProperties())
|
||||
.run();
|
||||
|
||||
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
|
||||
return relationshipCreationMonoNested.checkpoint()
|
||||
.then(processNestedRelations(targetNodeDescription, valueToBeSaved,
|
||||
inDatabase, stateMachine));
|
||||
} else {
|
||||
return relationshipCreationMonoNested.checkpoint().then();
|
||||
}
|
||||
}).checkpoint();
|
||||
});
|
||||
relationshipCreationMonos.add(createRelationship);
|
||||
}
|
||||
});
|
||||
|
||||
return Flux.concat(relationshipCreationMonos).checkpoint().then();
|
||||
});
|
||||
}
|
||||
|
||||
private <Y> Mono<Long> saveRelatedNode(Object relatedNode, Class<Y> entityType, NodeDescription targetNodeDescription,
|
||||
@Nullable String inDatabase) {
|
||||
|
||||
return determineDynamicLabels((Y) relatedNode, (Neo4jPersistentEntity<?>) targetNodeDescription, inDatabase)
|
||||
.flatMap(t -> {
|
||||
Y entity = t.getT1();
|
||||
DynamicLabels dynamicLabels = t.getT2();
|
||||
|
||||
return neo4jClient.query(() -> renderer.render(
|
||||
cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
|
||||
.in(inDatabase)
|
||||
.bind((Y) entity)
|
||||
.with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
|
||||
.fetchAs(Long.class).one();
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
if (((Neo4jPersistentEntity) targetNodeDescription).hasVersionProperty()) {
|
||||
return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
|
||||
}
|
||||
return Mono.empty();
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<DatabaseSelection> getDatabaseName() {
|
||||
|
||||
return this.databaseSelectionProvider.getDatabaseSelection().switchIfEmpty(Mono.just(undecided()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> preparedQuery) {
|
||||
|
||||
return getDatabaseName().map(databaseName -> {
|
||||
Class<T> resultType = preparedQuery.getResultType();
|
||||
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this
|
||||
.neo4jClient.query(preparedQuery.getCypherQuery())
|
||||
.in(databaseName.getValue())
|
||||
.bindAll(preparedQuery.getParameters())
|
||||
.fetchAs(resultType);
|
||||
|
||||
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery
|
||||
.getOptionalMappingFunction()
|
||||
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
|
||||
.orElse(mappingSpec);
|
||||
|
||||
return new DefaultReactiveExecutableQuery<>(fetchSpec);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
|
||||
this.eventSupport = new ReactiveNeo4jEvents(ReactiveEntityCallbacks.create(beanFactory));
|
||||
}
|
||||
|
||||
final class DefaultReactiveExecutableQuery<T> implements ExecutableQuery<T> {
|
||||
|
||||
private final ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec;
|
||||
|
||||
DefaultReactiveExecutableQuery(ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec) {
|
||||
this.fetchSpec = fetchSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All results returned by this query.
|
||||
*/
|
||||
public Flux<T> getResults() {
|
||||
return fetchSpec.all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A single result
|
||||
* @throws IncorrectResultSizeDataAccessException if there is no or more than one result
|
||||
*/
|
||||
public Mono<T> getSingleResult() {
|
||||
try {
|
||||
return fetchSpec.one();
|
||||
} catch (NoSuchRecordException e) {
|
||||
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
|
||||
// So there has been an incorrect result size, but not to few results but to many.
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class that orchestrates {@link ReactiveEntityCallbacks}.
|
||||
* All the methods provided here check for their availability and do nothing when an event cannot be published.
|
||||
*/
|
||||
final class ReactiveNeo4jEvents {
|
||||
|
||||
private final ReactiveEntityCallbacks entityCallbacks;
|
||||
|
||||
ReactiveNeo4jEvents(ReactiveEntityCallbacks entityCallbacks) {
|
||||
this.entityCallbacks = entityCallbacks;
|
||||
}
|
||||
|
||||
<T> Mono<T> maybeCallBeforeBind(T object) {
|
||||
return entityCallbacks.callback(ReactiveBeforeBindCallback.class, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity;
|
||||
import org.neo4j.springframework.data.core.schema.CypherGenerator;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* The {@link RelationshipStatementHolder} holds the Cypher Statement to create a relationship as well as the optional
|
||||
* properties that describe the relationship in case of more then 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
|
||||
* @since 1.0
|
||||
*/
|
||||
final class RelationshipStatementHolder {
|
||||
private final Statement relationshipCreationQuery;
|
||||
private final Map<String, Object> properties;
|
||||
|
||||
private RelationshipStatementHolder(@NonNull Statement relationshipCreationQuery) {
|
||||
this(relationshipCreationQuery, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private RelationshipStatementHolder(
|
||||
@NonNull Statement relationshipCreationQuery,
|
||||
@NonNull Map<String, Object> properties
|
||||
) {
|
||||
this.relationshipCreationQuery = relationshipCreationQuery;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
Statement getRelationshipCreationQuery() {
|
||||
return relationshipCreationQuery;
|
||||
}
|
||||
|
||||
Map<String, Object> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
static RelationshipStatementHolder createStatement(Neo4jMappingContext neo4jMappingContext,
|
||||
Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
NestedRelationshipContext relationshipContext,
|
||||
Long relatedInternalId,
|
||||
Object relatedValue) {
|
||||
|
||||
if (relationshipContext.hasRelationshipWithProperties()) {
|
||||
return createStatementForRelationShipWithProperties(neo4jMappingContext, neo4jPersistentEntity,
|
||||
relationshipContext, relatedInternalId, (Map.Entry) relatedValue);
|
||||
} else {
|
||||
return createStatementForRelationshipWithoutProperties(neo4jMappingContext, neo4jPersistentEntity,
|
||||
relationshipContext, relatedInternalId, relatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static RelationshipStatementHolder createStatementForRelationShipWithProperties(
|
||||
Neo4jMappingContext neo4jMappingContext,
|
||||
Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
NestedRelationshipContext relationshipContext,
|
||||
Long relatedInternalId,
|
||||
Map.Entry relatedValue) {
|
||||
|
||||
Statement relationshipCreationQuery = CypherGenerator.INSTANCE
|
||||
.createRelationshipWithPropertiesCreationQuery(
|
||||
neo4jPersistentEntity,
|
||||
relationshipContext.getRelationship(),
|
||||
relatedInternalId
|
||||
);
|
||||
Map<String, Object> propMap = new HashMap<>();
|
||||
neo4jMappingContext.getConverter().write(relatedValue.getValue(), propMap);
|
||||
|
||||
return new RelationshipStatementHolder(relationshipCreationQuery, propMap);
|
||||
}
|
||||
|
||||
private static RelationshipStatementHolder createStatementForRelationshipWithoutProperties(
|
||||
Neo4jMappingContext neo4jMappingContext,
|
||||
Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
NestedRelationshipContext relationshipContext,
|
||||
Long relatedInternalId,
|
||||
Object relatedValue
|
||||
) {
|
||||
|
||||
String relationshipType;
|
||||
if (!relationshipContext.getRelationship().isDynamic()) {
|
||||
relationshipType = null;
|
||||
} else {
|
||||
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation()
|
||||
.getRequiredComponentType();
|
||||
Object key = ((Map.Entry<?, ?>) relatedValue).getKey();
|
||||
relationshipType = neo4jMappingContext.getConverter().writeValueFromProperty(key, keyType).asString();
|
||||
}
|
||||
|
||||
Statement relationshipCreationQuery = CypherGenerator.INSTANCE
|
||||
.createRelationshipCreationQuery(neo4jPersistentEntity,
|
||||
relationshipContext.getRelationship(),
|
||||
relationshipType,
|
||||
relatedInternalId);
|
||||
return new RelationshipStatementHolder(relationshipCreationQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
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()}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @param <T> type of the domain class to map
|
||||
* @since 1.0
|
||||
*/
|
||||
final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Record, T> {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
SingleValueMappingFunction(ConversionService conversionService,
|
||||
Class<T> targetClass) {
|
||||
this.conversionService = conversionService;
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T apply(TypeSystem typeSystem, Record record) {
|
||||
|
||||
if (record.size() == 0) {
|
||||
throw new IllegalArgumentException("Record has no elements, cannot map nothing.");
|
||||
}
|
||||
|
||||
if (record.size() > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Records with more than one value cannot be converted without a mapper.");
|
||||
}
|
||||
|
||||
Value source = record.get(0);
|
||||
return source == null || source == Values.NULL ? null : conversionService.convert(source, targetClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.exceptions.value.LossyCoercion;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalConverter;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.util.Assert;
|
||||
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.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
final class AdditionalTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<Object> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, boolean[].class, AdditionalTypes::asBooleanArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, Character.class, AdditionalTypes::asCharacter).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, char.class, AdditionalTypes::asCharacter).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, char[].class, AdditionalTypes::asCharArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Date.class, AdditionalTypes::asDate).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, double[].class, AdditionalTypes::asDoubleArray).andWriting(Values::value));
|
||||
hlp.add(new EnumConverter());
|
||||
hlp.add(reading(Value.class, Float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, float[].class, AdditionalTypes::asFloatArray).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, Integer.class, Value::asInt).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, int.class, Value::asInt).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, int[].class, AdditionalTypes::asIntArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Locale.class, AdditionalTypes::asLocale).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, long[].class, AdditionalTypes::asLongArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, short[].class, AdditionalTypes::asShortArray).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, String[].class, AdditionalTypes::asStringArray).andWriting(Values::value));
|
||||
hlp.add(
|
||||
reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal).andWriting(AdditionalTypes::value));
|
||||
hlp.add(
|
||||
reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger).andWriting(AdditionalTypes::value));
|
||||
hlp.add(
|
||||
reading(Value.class, TemporalAmount.class, AdditionalTypes::asTemporalAmount)
|
||||
.andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, Instant.class, AdditionalTypes::asInstant).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, UUID.class, AdditionalTypes::asUUID).andWriting(AdditionalTypes::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
static UUID asUUID(Value value) {
|
||||
return UUID.fromString(value.asString());
|
||||
}
|
||||
|
||||
static Value value(UUID uuid) {
|
||||
if (uuid == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(uuid.toString());
|
||||
}
|
||||
|
||||
static Instant asInstant(Value value) {
|
||||
return value.asZonedDateTime().toInstant();
|
||||
}
|
||||
|
||||
static Value value(Instant instant) {
|
||||
return Values.value(instant.atOffset(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
static TemporalAmount asTemporalAmount(Value value) {
|
||||
return new TemporalAmountAdapter().apply(value.asIsoDuration());
|
||||
}
|
||||
|
||||
static Value value(TemporalAmount temporalAmount) {
|
||||
return Values.value(temporalAmount);
|
||||
}
|
||||
|
||||
static BigDecimal asBigDecimal(Value value) {
|
||||
return new BigDecimal(value.asString());
|
||||
}
|
||||
|
||||
static Value value(BigDecimal bigDecimal) {
|
||||
if (bigDecimal == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(bigDecimal.toString());
|
||||
}
|
||||
|
||||
static BigInteger asBigInteger(Value value) {
|
||||
return new BigInteger(value.asString());
|
||||
}
|
||||
|
||||
static Value value(BigInteger bigInteger) {
|
||||
if (bigInteger == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(bigInteger.toString());
|
||||
}
|
||||
|
||||
static Byte asByte(Value value) {
|
||||
byte[] bytes = value.asByteArray();
|
||||
Assert.isTrue(bytes.length == 1, "Expected a byte array with exactly 1 element.");
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
static Value value(Byte aByte) {
|
||||
if (aByte == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(new Byte[] { aByte });
|
||||
}
|
||||
|
||||
static Character asCharacter(Value value) {
|
||||
char[] chars = value.asString().toCharArray();
|
||||
Assert.isTrue(chars.length == 1, "Expected a char array with exactly 1 element.");
|
||||
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));
|
||||
}
|
||||
|
||||
static Value value(Date date) {
|
||||
if (date == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(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 after thought 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
|
||||
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) {
|
||||
return typeDescriptor.isArray() && Enum.class
|
||||
.isAssignableFrom(typeDescriptor.getElementTypeDescriptor().getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(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 = 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, sourceType.getElementTypeDescriptor(), TypeDescriptor.valueOf(Value.class))).toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Float asFloat(Value value) {
|
||||
return Float.parseFloat(value.asString());
|
||||
}
|
||||
|
||||
static Value value(Float aFloat) {
|
||||
if (aFloat == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(aFloat.toString());
|
||||
}
|
||||
|
||||
static Locale asLocale(Value value) {
|
||||
|
||||
return StringUtils.parseLocale(value.asString());
|
||||
}
|
||||
|
||||
static Value value(Locale locale) {
|
||||
if (locale == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(locale.toString());
|
||||
}
|
||||
|
||||
static Short asShort(Value value) {
|
||||
long val = value.asLong();
|
||||
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
|
||||
throw new LossyCoercion(value.type().name(), "Java short");
|
||||
}
|
||||
return (short) val;
|
||||
}
|
||||
|
||||
static Value value(Short aShort) {
|
||||
if (aShort == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(aShort.longValue());
|
||||
}
|
||||
|
||||
static boolean[] asBooleanArray(Value value) {
|
||||
boolean[] array = new boolean[value.size()];
|
||||
int i = 0;
|
||||
for (Boolean v : value.values(Value::asBoolean)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static char[] asCharArray(Value value) {
|
||||
char[] array = new char[value.size()];
|
||||
int i = 0;
|
||||
for (Character v : value.values(AdditionalTypes::asCharacter)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static String[] asStringArray(Value value) {
|
||||
String[] array = new String[value.size()];
|
||||
return value.asList(Value::asString).toArray(array);
|
||||
}
|
||||
|
||||
static double[] asDoubleArray(Value value) {
|
||||
double[] array = new double[value.size()];
|
||||
int i = 0;
|
||||
for (double v : value.values(Value::asDouble)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static float[] asFloatArray(Value value) {
|
||||
float[] array = new float[value.size()];
|
||||
int i = 0;
|
||||
for (float v : value.values(AdditionalTypes::asFloat)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(float[] aFloatArray) {
|
||||
if (aFloatArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
String[] values = new String[aFloatArray.length];
|
||||
int i = 0;
|
||||
for (float v : aFloatArray) {
|
||||
values[i++] = Float.toString(v);
|
||||
}
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
static int[] asIntArray(Value value) {
|
||||
int[] array = new int[value.size()];
|
||||
int i = 0;
|
||||
for (int v : value.values(Value::asInt)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static long[] asLongArray(Value value) {
|
||||
long[] array = new long[value.size()];
|
||||
int i = 0;
|
||||
for (long v : value.values(Value::asLong)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static short[] asShortArray(Value value) {
|
||||
short[] array = new short[value.size()];
|
||||
int i = 0;
|
||||
for (short v : value.values(AdditionalTypes::asShort)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(short[] aShortArray) {
|
||||
if (aShortArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
long[] values = new long[aShortArray.length];
|
||||
int i = 0;
|
||||
for (short v : aShortArray) {
|
||||
values[i++] = v;
|
||||
}
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
private AdditionalTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.IsoDuration;
|
||||
import org.neo4j.driver.types.Point;
|
||||
|
||||
/**
|
||||
* Conversions for all known Cypher types, directly supported by the driver.
|
||||
* See <a href="https://neo4j.com/docs/driver-manual/current/cypher-values/">Working with Cypher values</a>.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class CypherTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<ConverterAware> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, Void.class, v -> null).andWriting(v -> Values.NULL));
|
||||
hlp.add(reading(Value.class, void.class, v -> null).andWriting(v -> Values.NULL));
|
||||
hlp.add(reading(Value.class, Boolean.class, Value::asBoolean).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, boolean.class, Value::asBoolean).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Long.class, Value::asLong).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, long.class, Value::asLong).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Double.class, Value::asDouble).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, double.class, Value::asDouble).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, String.class, Value::asString).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, byte[].class, Value::asByteArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalDate.class, Value::asLocalDate).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, OffsetTime.class, Value::asOffsetTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalTime.class, Value::asLocalTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, ZonedDateTime.class, Value::asZonedDateTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalDateTime.class, Value::asLocalDateTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, IsoDuration.class, Value::asIsoDuration).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Point.class, Value::asPoint).andWriting(Values::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
private CypherTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.convert.AdditionalTypes.EnumArrayConverter;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class Neo4jConversions extends CustomConversions {
|
||||
|
||||
private static final StoreConversions STORE_CONVERSIONS;
|
||||
private static final List<Object> STORE_CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<Object> converters = new ArrayList<>();
|
||||
|
||||
converters.addAll(CypherTypes.CONVERTERS);
|
||||
converters.addAll(AdditionalTypes.CONVERTERS);
|
||||
converters.addAll(SpatialTypes.CONVERTERS);
|
||||
|
||||
STORE_CONVERTERS = Collections.unmodifiableList(converters);
|
||||
STORE_CONVERSIONS = StoreConversions.of(Neo4jSimpleTypes.HOLDER, STORE_CONVERTERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Neo4jConversions} object without custom converters.
|
||||
*/
|
||||
public Neo4jConversions() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link CustomConversions} instance registering the given converters.
|
||||
*
|
||||
* @param converters must not be {@literal null}.
|
||||
*/
|
||||
public Neo4jConversions(Collection<?> converters) {
|
||||
super(STORE_CONVERSIONS, converters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerConvertersIn(ConverterRegistry conversionService) {
|
||||
super.registerConvertersIn(conversionService);
|
||||
conversionService.addConverter(new EnumArrayConverter());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.springframework.dao.TypeMismatchDataAccessException;
|
||||
import org.springframework.data.convert.EntityReader;
|
||||
import org.springframework.data.convert.EntityWriter;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* This orchestrates the build-in store conversions and any additional Spring converters.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface Neo4jConverter extends EntityReader<Object, Record>, EntityWriter<Object, Map<String, Object>> {
|
||||
|
||||
/**
|
||||
* Reads a {@link Value} returned by the driver and converts it into a {@link Neo4jSimpleTypes simple type} supported
|
||||
* by Neo4j SDN/RX.
|
||||
* If the value cannot be converted, a {@link TypeMismatchDataAccessException} will be thrown, it's cause indicating
|
||||
* the failed conversion.
|
||||
*
|
||||
* @param value The value to be read, may be null.
|
||||
* @param type The type information describing the target type.
|
||||
* @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 readValueForProperty(@Nullable Value value, TypeInformation<?> type);
|
||||
|
||||
/**
|
||||
* Converts an {@link Object} to a driver's value object.
|
||||
*
|
||||
* @param value The value to get written, may be null.
|
||||
* @param type The type information describing the target type.
|
||||
* @return A driver compatible value object.
|
||||
*/
|
||||
Value writeValueFromProperty(@Nullable Object value, TypeInformation<?> type);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.types.IsoDuration;
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
|
||||
/**
|
||||
* 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>.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class Neo4jSimpleTypes {
|
||||
|
||||
private static final Set<Class<?>> NEO4J_NATIVE_TYPES;
|
||||
|
||||
static {
|
||||
Set<Class<?>> neo4jNativeTypes = new HashSet<>();
|
||||
|
||||
neo4jNativeTypes.add(Instant.class);
|
||||
neo4jNativeTypes.add(IsoDuration.class);
|
||||
neo4jNativeTypes.add(LocalDate.class);
|
||||
neo4jNativeTypes.add(LocalDateTime.class);
|
||||
neo4jNativeTypes.add(LocalTime.class);
|
||||
neo4jNativeTypes.add(Map.class);
|
||||
neo4jNativeTypes.add(OffsetTime.class);
|
||||
neo4jNativeTypes.add(Point.class);
|
||||
neo4jNativeTypes.add(Void.class);
|
||||
neo4jNativeTypes.add(ZonedDateTime.class);
|
||||
neo4jNativeTypes.add(void.class);
|
||||
neo4jNativeTypes.add(UUID.class);
|
||||
|
||||
neo4jNativeTypes.add(BigDecimal.class);
|
||||
neo4jNativeTypes.add(BigInteger.class);
|
||||
|
||||
neo4jNativeTypes.add(org.springframework.data.geo.Point.class);
|
||||
neo4jNativeTypes.add(GeographicPoint2d.class);
|
||||
neo4jNativeTypes.add(GeographicPoint3d.class);
|
||||
neo4jNativeTypes.add(CartesianPoint2d.class);
|
||||
neo4jNativeTypes.add(CartesianPoint3d.class);
|
||||
|
||||
neo4jNativeTypes.add(Value.class);
|
||||
|
||||
NEO4J_NATIVE_TYPES = Collections.unmodifiableSet(neo4jNativeTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* The simple types we support plus all the simple types recognized by Spring.
|
||||
*/
|
||||
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true);
|
||||
|
||||
private Neo4jSimpleTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.Coordinate;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.neo4j.springframework.data.types.Neo4jPoint;
|
||||
import org.neo4j.springframework.data.types.PointBuilder;
|
||||
import org.springframework.data.convert.ConverterBuilder;
|
||||
import org.springframework.data.geo.Point;
|
||||
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.
|
||||
* <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.
|
||||
* <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.
|
||||
* <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.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class SpatialTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<ConverterBuilder.ConverterAware> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint)
|
||||
.andWriting(SpatialTypes::value));
|
||||
hlp.add(reading(Value.class, Point[].class, SpatialTypes::asPointArray)
|
||||
.andWriting(SpatialTypes::value));
|
||||
|
||||
hlp.add(reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint)
|
||||
.andWriting(SpatialTypes::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
static Neo4jPoint asNeo4jPoint(Value value) {
|
||||
|
||||
org.neo4j.driver.types.Point point = value.asPoint();
|
||||
|
||||
Coordinate coordinate = new Coordinate(point.x(), point.y(), Double.isNaN(point.z()) ? null : point.z());
|
||||
return PointBuilder.withSrid(point.srid()).build(coordinate);
|
||||
}
|
||||
|
||||
static Value value(Neo4jPoint object) {
|
||||
|
||||
if (object instanceof CartesianPoint2d) {
|
||||
CartesianPoint2d point = (CartesianPoint2d) object;
|
||||
return Values.point(point.getSrid(), point.getX(), point.getY());
|
||||
} 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) {
|
||||
GeographicPoint2d point = (GeographicPoint2d) object;
|
||||
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude());
|
||||
} else if (object instanceof GeographicPoint3d) {
|
||||
GeographicPoint3d point = (GeographicPoint3d) object;
|
||||
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude(),
|
||||
point.getHeight());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported point implementation: " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
static Point asSpringDataPoint(Value value) {
|
||||
|
||||
org.neo4j.driver.types.Point point = value.asPoint();
|
||||
Assert.isTrue(point.srid() == 4326, "Srid must be 4326");
|
||||
|
||||
return new Point(point.y(), point.x());
|
||||
}
|
||||
|
||||
static Value value(Point point) {
|
||||
return Values.point(4326, point.getY(), point.getX());
|
||||
}
|
||||
|
||||
static Point[] asPointArray(Value value) {
|
||||
Point[] array = new Point[value.size()];
|
||||
int i = 0;
|
||||
for (Point v : value.values(SpatialTypes::asSpringDataPoint)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(Point[] aPointArray) {
|
||||
if (aPointArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
Value[] values = new Value[aPointArray.length];
|
||||
int i = 0;
|
||||
for (Point v : aPointArray) {
|
||||
values[i++] = value(v);
|
||||
}
|
||||
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
private SpatialTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Period;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalUnit;
|
||||
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, than 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 {@link TemporalAmount} in this 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.
|
||||
*
|
||||
* @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) -> {
|
||||
if (!d.getUnits().contains(u)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.toIntExact(d.get(u));
|
||||
};
|
||||
|
||||
@Override
|
||||
public TemporalAmount apply(TemporalAmount internalTemporalAmountRepresentation) {
|
||||
|
||||
int[] values = new int[SUPPORTED_UNITS.length];
|
||||
int type = 0;
|
||||
for (int i = 0; i < SUPPORTED_UNITS.length; ++i) {
|
||||
values[i] = TEMPORAL_UNIT_EXTRACTOR.apply(internalTemporalAmountRepresentation, SUPPORTED_UNITS[i]);
|
||||
type |= (values[i] == 0) ? 0 : (0b10000 >> i);
|
||||
}
|
||||
|
||||
boolean couldBePeriod = couldBePeriod(type);
|
||||
boolean couldBeDuration = couldBeDuration(type);
|
||||
|
||||
if (couldBePeriod && !couldBeDuration) {
|
||||
return Period.of(values[FIELD_YEAR], values[FIELD_MONTH], values[FIELD_DAY]).normalized();
|
||||
} else if (couldBeDuration && !couldBePeriod) {
|
||||
return Duration.ofSeconds(values[FIELD_SECONDS]).plusNanos(values[FIELD_NANOS]);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Neo4j-specific conversion classes.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,574 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
import static org.neo4j.springframework.data.core.schema.RelationshipDescription.*;
|
||||
import static org.springframework.core.CollectionFactory.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
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.Relationship;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.dao.TypeMismatchDataAccessException;
|
||||
import org.springframework.data.convert.EntityInstantiators;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @author Philipp Tölle
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
final class DefaultNeo4jConverter implements Neo4jConverter {
|
||||
|
||||
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jConverter.class));
|
||||
|
||||
/**
|
||||
* The shared entity instantiators of this context. Those should not be recreated for each entity or even not for
|
||||
* each query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator
|
||||
* won't apply
|
||||
*/
|
||||
private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators();
|
||||
|
||||
private final NodeDescriptionStore nodeDescriptionStore;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private TypeSystem typeSystem;
|
||||
|
||||
DefaultNeo4jConverter(Neo4jConversions neo4jConversions, NodeDescriptionStore nodeDescriptionStore) {
|
||||
|
||||
Assert.notNull(neo4jConversions, "Neo4jConversions must not be null!");
|
||||
|
||||
final ConfigurableConversionService configurableConversionService = new DefaultConversionService();
|
||||
neo4jConversions.registerConvertersIn(configurableConversionService);
|
||||
|
||||
this.conversionService = configurableConversionService;
|
||||
this.nodeDescriptionStore = nodeDescriptionStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R read(Class<R> targetType, Record record) {
|
||||
|
||||
Neo4jPersistentEntity<R> rootNodeDescription =
|
||||
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType);
|
||||
|
||||
try {
|
||||
List<Value> recordValues = record.values();
|
||||
String nodeLabel = rootNodeDescription.getPrimaryLabel();
|
||||
MapAccessor queryRoot = null;
|
||||
for (Value value : recordValues) {
|
||||
if (value.hasType(typeSystem.NODE()) && value.asNode().hasLabel(nodeLabel)) {
|
||||
if (recordValues.size() > 1) {
|
||||
queryRoot = mergeRootNodeWithRecord(value.asNode(), record);
|
||||
} else {
|
||||
queryRoot = value.asNode();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (queryRoot == null) {
|
||||
for (Value value : recordValues) {
|
||||
if (value.hasType(typeSystem.MAP())) {
|
||||
queryRoot = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queryRoot == null) {
|
||||
log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record,
|
||||
rootNodeDescription));
|
||||
return null; // todo should not be null because of the @nonnullapi annotation in the EntityReader. Fail?
|
||||
} else {
|
||||
return map(queryRoot, rootNodeDescription, new KnownObjects());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new MappingException("Error mapping " + record.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object readValueForProperty(@Nullable Value value, TypeInformation<?> type) {
|
||||
|
||||
boolean valueIsLiteralNullOrNullValue = value == null || value == Values.NULL;
|
||||
|
||||
try {
|
||||
Class<?> rawType = type.getType();
|
||||
|
||||
if (!valueIsLiteralNullOrNullValue && isCollection(type)) {
|
||||
Collection<Object> target = createCollection(rawType, type.getComponentType().getType(), value.size());
|
||||
value.values().forEach(
|
||||
element -> target.add(conversionService.convert(element, type.getComponentType().getType())));
|
||||
return target;
|
||||
}
|
||||
|
||||
return conversionService.convert(valueIsLiteralNullOrNullValue ? null : value, rawType);
|
||||
} catch (Exception e) {
|
||||
String msg = String.format("Could not convert %s into %s", value, type.toString());
|
||||
throw new TypeMismatchDataAccessException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<String> createDynamicLabelsProperty(TypeInformation<?> type, Collection<String> dynamicLabels) {
|
||||
|
||||
Collection<String> target = createCollection(type.getType(), String.class, dynamicLabels.size());
|
||||
target.addAll(dynamicLabels);
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(Object source, Map<String, Object> parameters) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
Neo4jPersistentEntity<?> nodeDescription =
|
||||
(Neo4jPersistentEntity<?>) nodeDescriptionStore.getNodeDescription(source.getClass());
|
||||
|
||||
PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source);
|
||||
nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> {
|
||||
|
||||
// Skip the internal properties, we don't want them to end up stored as properties
|
||||
if (p.isInternalIdProperty() || p.isDynamicLabels()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Object value = writeValueFromProperty(propertyAccessor.getProperty(p), p.getTypeInformation());
|
||||
properties.put(p.getPropertyName(), value);
|
||||
});
|
||||
|
||||
parameters.put(NAME_OF_PROPERTIES_PARAM, properties);
|
||||
|
||||
// in case of relationship properties ignore internal id property
|
||||
if (nodeDescription.hasIdProperty()) {
|
||||
Neo4jPersistentProperty idProperty = nodeDescription.getRequiredIdProperty();
|
||||
parameters.put(NAME_OF_ID,
|
||||
writeValueFromProperty(propertyAccessor.getProperty(idProperty), idProperty.getTypeInformation()));
|
||||
}
|
||||
// in case of relationship properties ignore internal id property
|
||||
if (nodeDescription.hasVersionProperty()) {
|
||||
Long versionProperty = (Long) propertyAccessor.getProperty(nodeDescription.getRequiredVersionProperty());
|
||||
|
||||
// we incremented this upfront the persist operation so the matching version would be one "before"
|
||||
parameters.put(NAME_OF_VERSION_PARAM, versionProperty - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value writeValueFromProperty(@Nullable Object value, TypeInformation<?> type) {
|
||||
|
||||
if (value == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
if (isCollection(type)) {
|
||||
Collection<?> sourceCollection = (Collection<?>) value;
|
||||
Object[] targetCollection = (sourceCollection).stream().map(element ->
|
||||
conversionService.convert(element, Value.class)).toArray();
|
||||
return Values.value(targetCollection);
|
||||
}
|
||||
|
||||
return conversionService.convert(value, Value.class);
|
||||
}
|
||||
|
||||
private static boolean isCollection(TypeInformation<?> type) {
|
||||
return Collection.class.isAssignableFrom(type.getType());
|
||||
}
|
||||
|
||||
void setTypeSystem(TypeSystem typeSystem) {
|
||||
this.typeSystem = typeSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too.
|
||||
* Merge happens only when the record contains additional values.
|
||||
*
|
||||
* @param node Node whose attributes are about to be merged
|
||||
* @param record Record that should be merged
|
||||
* @return
|
||||
*/
|
||||
private static MapAccessor mergeRootNodeWithRecord(Node node, Record record) {
|
||||
Map<String, Object> mergedAttributes = new HashMap<>(node.size() + record.size() + 1);
|
||||
|
||||
mergedAttributes.put(NAME_OF_INTERNAL_ID, node.id());
|
||||
mergedAttributes.putAll(node.asMap(Function.identity()));
|
||||
mergedAttributes.putAll(record.asMap(Function.identity()));
|
||||
|
||||
return Values.value(mergedAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param queryResult The original query result
|
||||
* @param nodeDescription The node description of the current entity to be mapped from the result
|
||||
* @param knownObjects The current list of known objects
|
||||
* @param <ET> As in entity type
|
||||
* @return
|
||||
*/
|
||||
private <ET> ET map(MapAccessor queryResult,
|
||||
Neo4jPersistentEntity<ET> nodeDescription,
|
||||
KnownObjects knownObjects) {
|
||||
|
||||
List<String> allLabels = getLabels(queryResult);
|
||||
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
|
||||
.deriveConcreteNodeDescription(nodeDescription, allLabels);
|
||||
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
|
||||
.getNodeDescription();
|
||||
|
||||
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
|
||||
|
||||
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
|
||||
nodeDescriptionAndLabels.getDynamicLabels());
|
||||
|
||||
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
|
||||
|
||||
if (concreteNodeDescription.requiresPropertyPopulation()) {
|
||||
|
||||
// Fill simple properties
|
||||
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
|
||||
.getPersistenceConstructor()::isConstructorParameter;
|
||||
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(
|
||||
queryResult, propertyAccessor, isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels());
|
||||
concreteNodeDescription.doWithProperties(handler);
|
||||
|
||||
// Fill associations
|
||||
concreteNodeDescription.doWithAssociations(
|
||||
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of labels for the entity to be created from the "main" node returned.
|
||||
*
|
||||
* @param queryResult The complete query result
|
||||
* @return The list of labels defined by the query variable {@link org.neo4j.springframework.data.core.schema.Constants#NAME_OF_LABELS}.
|
||||
*/
|
||||
@NonNull
|
||||
private List<String> getLabels(MapAccessor queryResult) {
|
||||
Value labelsValue = queryResult.get(NAME_OF_LABELS);
|
||||
List<String> labels = new ArrayList<>();
|
||||
if (!labelsValue.isNull()) {
|
||||
labels = labelsValue.asList(Value::asString);
|
||||
} else if (queryResult instanceof Node) {
|
||||
Node nodeRepresentation = (Node) queryResult;
|
||||
nodeRepresentation.labels().forEach(labels::add);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription,
|
||||
MapAccessor values,
|
||||
KnownObjects knownObjects,
|
||||
Collection<RelationshipDescription> relationships,
|
||||
Collection<String> surplusLabels) {
|
||||
|
||||
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
|
||||
@Override
|
||||
public Object getParameterValue(PreferredConstructor.Parameter parameter) {
|
||||
|
||||
Neo4jPersistentProperty matchingProperty = nodeDescription
|
||||
.getRequiredPersistentProperty(parameter.getName());
|
||||
|
||||
if (matchingProperty.isRelationship()) {
|
||||
return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships)
|
||||
.orElse(null);
|
||||
} else if (matchingProperty.isDynamicLabels()) {
|
||||
return createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels);
|
||||
}
|
||||
return readValueForProperty(extractValueOf(matchingProperty, values), parameter.getType());
|
||||
}
|
||||
};
|
||||
|
||||
return INSTANTIATORS.getInstantiatorFor(nodeDescription)
|
||||
.createInstance(nodeDescription, parameterValueProvider);
|
||||
}
|
||||
|
||||
private PropertyHandler<Neo4jPersistentProperty> populateFrom(
|
||||
MapAccessor queryResult,
|
||||
PersistentPropertyAccessor<?> propertyAccessor,
|
||||
Predicate<Neo4jPersistentProperty> isConstructorParameter,
|
||||
Collection<String> surplusLabels
|
||||
) {
|
||||
return property -> {
|
||||
if (isConstructorParameter.test(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.isDynamicLabels()) {
|
||||
propertyAccessor
|
||||
.setProperty(property, createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels));
|
||||
} else {
|
||||
propertyAccessor.setProperty(property,
|
||||
readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private AssociationHandler<Neo4jPersistentProperty> populateFrom(
|
||||
MapAccessor queryResult,
|
||||
PersistentPropertyAccessor<?> propertyAccessor,
|
||||
Predicate<Neo4jPersistentProperty> isConstructorParameter,
|
||||
Collection<RelationshipDescription> relationships,
|
||||
KnownObjects knownObjects
|
||||
) {
|
||||
return association -> {
|
||||
|
||||
Neo4jPersistentProperty persistentProperty = association.getInverse();
|
||||
if (isConstructorParameter.test(persistentProperty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
createInstanceOfRelationships(persistentProperty, queryResult, knownObjects, relationships)
|
||||
.ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value));
|
||||
};
|
||||
}
|
||||
|
||||
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty,
|
||||
MapAccessor values,
|
||||
KnownObjects knownObjects,
|
||||
Collection<RelationshipDescription> relationshipDescriptions) {
|
||||
|
||||
RelationshipDescription relationshipDescription = relationshipDescriptions.stream()
|
||||
.filter(r -> r.getFieldName().equals(persistentProperty.getName()))
|
||||
.findFirst().get();
|
||||
|
||||
String relationshipType = relationshipDescription.getType();
|
||||
String targetLabel = relationshipDescription.getTarget().getPrimaryLabel();
|
||||
|
||||
Neo4jPersistentEntity<?> genericTargetNodeDescription =
|
||||
(Neo4jPersistentEntity<?>) relationshipDescription.getTarget();
|
||||
|
||||
List<String> allLabels = getLabels(values);
|
||||
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
|
||||
.deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels);
|
||||
Neo4jPersistentEntity<?> concreteTargetNodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionAndLabels
|
||||
.getNodeDescription();
|
||||
|
||||
List<Object> value = new ArrayList<>();
|
||||
Map<Object, Object> dynamicValue = new HashMap<>();
|
||||
|
||||
BiConsumer<String, Object> mappedObjectHandler;
|
||||
Function<String, ?> keyTransformer;
|
||||
if (persistentProperty.isDynamicAssociation() && persistentProperty.getComponentType().isEnum()) {
|
||||
keyTransformer = f -> conversionService.convert(f, persistentProperty.getComponentType());
|
||||
} else {
|
||||
keyTransformer = Function.identity();
|
||||
}
|
||||
if (persistentProperty.isDynamicOneToManyAssociation()) {
|
||||
|
||||
TypeInformation<?> actualType = persistentProperty.getTypeInformation().getRequiredActualType();
|
||||
mappedObjectHandler = (type, mappedObject) -> {
|
||||
List<Object> bucket = (List<Object>) dynamicValue.computeIfAbsent(keyTransformer.apply(type),
|
||||
s -> createCollection(actualType.getType(), persistentProperty.getAssociationTargetType(),
|
||||
values.size()));
|
||||
bucket.add(mappedObject);
|
||||
};
|
||||
} else if (persistentProperty.isDynamicAssociation()) {
|
||||
mappedObjectHandler = (type, mappedObject) -> dynamicValue.put(keyTransformer.apply(type), mappedObject);
|
||||
} else {
|
||||
mappedObjectHandler = (type, mappedObject) -> value.add(mappedObject);
|
||||
}
|
||||
|
||||
Value list = values.get(relationshipDescription.generateRelatedNodesCollectionName());
|
||||
|
||||
Map<Object, Object> relationshipsAndProperties = new HashMap<>();
|
||||
|
||||
// if the list is null the mapping is based on a custom query
|
||||
if (list == Values.NULL) {
|
||||
|
||||
Predicate<Value> isList = entry -> entry instanceof Value && typeSystem.LIST().isTypeOf(entry);
|
||||
|
||||
Predicate<Value> containsOnlyRelationships = entry -> entry.asList(Function.identity())
|
||||
.stream()
|
||||
.allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry));
|
||||
|
||||
Predicate<Value> containsOnlyNodes = entry -> entry.asList(Function.identity())
|
||||
.stream()
|
||||
.allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry));
|
||||
|
||||
// find relationships in the result
|
||||
List<Relationship> allMatchingTypeRelationshipsInResult = StreamSupport
|
||||
.stream(values.values().spliterator(), false)
|
||||
.filter(isList.and(containsOnlyRelationships))
|
||||
.flatMap(entry -> entry.asList(Value::asRelationship).stream())
|
||||
.filter(r -> r.type().equals(relationshipType))
|
||||
.collect(toList());
|
||||
|
||||
List<Node> allNodesWithMatchingLabelInResult = StreamSupport
|
||||
.stream(values.values().spliterator(), false)
|
||||
.filter(isList.and(containsOnlyNodes))
|
||||
.flatMap(entry -> entry.asList(Value::asNode).stream())
|
||||
.filter(n -> n.hasLabel(targetLabel))
|
||||
.collect(toList());
|
||||
|
||||
if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
for (Node possibleValueNode : allNodesWithMatchingLabelInResult) {
|
||||
long nodeId = possibleValueNode.id();
|
||||
|
||||
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
|
||||
if (possibleRelationship.endNodeId() == nodeId) {
|
||||
Object mappedObject = map(possibleValueNode, concreteTargetNodeDescription, knownObjects);
|
||||
if (relationshipDescription.hasRelationshipProperties()) {
|
||||
|
||||
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
|
||||
|
||||
Object relationshipProperties = map(possibleRelationship,
|
||||
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass),
|
||||
knownObjects);
|
||||
relationshipsAndProperties.put(mappedObject, relationshipProperties);
|
||||
} else {
|
||||
mappedObjectHandler.accept(possibleRelationship.type(), mappedObject);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Value relatedEntity : list.asList(Function.identity())) {
|
||||
Neo4jPersistentProperty idProperty = concreteTargetNodeDescription.getRequiredIdProperty();
|
||||
|
||||
// internal (generated) id or external set
|
||||
String relatedEntityIdKey = idProperty.isInternalIdProperty()
|
||||
? NAME_OF_INTERNAL_ID
|
||||
: concreteTargetNodeDescription.getIdDescription()
|
||||
.getOptionalGraphPropertyName()
|
||||
.orElse(idProperty.getName());
|
||||
Object idValue = relatedEntity.get(relatedEntityIdKey);
|
||||
|
||||
Object valueEntry = knownObjects.computeIfAbsent(idValue,
|
||||
() -> map(relatedEntity, concreteTargetNodeDescription, knownObjects));
|
||||
|
||||
if (relationshipDescription.hasRelationshipProperties()) {
|
||||
Relationship relatedEntityRelationship = relatedEntity.get(NAME_OF_RELATIONSHIP).asRelationship();
|
||||
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
|
||||
|
||||
Object relationshipProperties = map(relatedEntityRelationship,
|
||||
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass),
|
||||
knownObjects);
|
||||
relationshipsAndProperties.put(valueEntry, relationshipProperties);
|
||||
} else {
|
||||
mappedObjectHandler.accept(relatedEntity.get(NAME_OF_RELATIONSHIP_TYPE).asString(), valueEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (persistentProperty.getTypeInformation().isCollectionLike()) {
|
||||
if (persistentProperty.getType().equals(Set.class)) {
|
||||
return Optional.of(new HashSet(value));
|
||||
} else {
|
||||
return Optional.of(value);
|
||||
}
|
||||
} else {
|
||||
if (relationshipDescription.isDynamic()) {
|
||||
return Optional.ofNullable(dynamicValue.isEmpty() ? null : dynamicValue);
|
||||
} else if (relationshipDescription.hasRelationshipProperties()) {
|
||||
return Optional.of(relationshipsAndProperties);
|
||||
} else {
|
||||
return Optional.ofNullable(value.isEmpty() ? null : value.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
|
||||
if (property.isInternalIdProperty()) {
|
||||
return propertyContainer instanceof Node ?
|
||||
Values.value(((Node) propertyContainer).id()) :
|
||||
propertyContainer.get(NAME_OF_INTERNAL_ID);
|
||||
} else {
|
||||
String graphPropertyName = property.getPropertyName();
|
||||
return propertyContainer.get(graphPropertyName);
|
||||
}
|
||||
}
|
||||
|
||||
static class KnownObjects {
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final Lock read = lock.readLock();
|
||||
private final Lock write = lock.writeLock();
|
||||
|
||||
private Map<Object, Object> store = new HashMap<>();
|
||||
|
||||
Object computeIfAbsent(Object key, Supplier<Object> entitySupplier) {
|
||||
try {
|
||||
|
||||
read.lock();
|
||||
|
||||
Object knownEntity = store.get(key);
|
||||
|
||||
if (knownEntity != null) {
|
||||
return knownEntity;
|
||||
}
|
||||
|
||||
} finally {
|
||||
read.unlock();
|
||||
}
|
||||
|
||||
try {
|
||||
write.lock();
|
||||
Object computedEntity = entitySupplier.get();
|
||||
store.put(key, computedEntity);
|
||||
return computedEntity;
|
||||
} finally {
|
||||
write.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.neo4j.springframework.data.core.schema.IdDescription;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.data.support.IsNewStrategy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 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 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>
|
||||
* </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>
|
||||
* </ul>
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
|
||||
|
||||
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jIsNewStrategy.class));
|
||||
|
||||
static IsNewStrategy basedOn(Neo4jPersistentEntity<?> entityMetaData) {
|
||||
|
||||
Assert.notNull(entityMetaData, "Entity meta data must not be null.");
|
||||
|
||||
IdDescription idDescription = entityMetaData.getIdDescription();
|
||||
Class<?> valueType = entityMetaData.getRequiredIdProperty().getType();
|
||||
|
||||
if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) {
|
||||
throw new IllegalArgumentException(String.format("Cannot use %s with externally generated, primitive ids.",
|
||||
DefaultNeo4jIsNewStrategy.class.getName()));
|
||||
}
|
||||
|
||||
Function<Object, Object> valueLookup;
|
||||
Neo4jPersistentProperty versionProperty = entityMetaData.getVersionProperty();
|
||||
if (idDescription.isAssignedId()) {
|
||||
if (versionProperty == null) {
|
||||
log.warn(() -> "Instances of " + entityMetaData.getType()
|
||||
+ " with an assigned id will always be treated as new without version property!");
|
||||
valueType = Void.class;
|
||||
valueLookup = source -> null;
|
||||
} else {
|
||||
valueType = versionProperty.getType();
|
||||
valueLookup = source -> entityMetaData.getPropertyAccessor(source).getProperty(versionProperty);
|
||||
}
|
||||
} else {
|
||||
valueLookup = source -> entityMetaData.getIdentifierAccessor(source).getIdentifier();
|
||||
}
|
||||
|
||||
return new DefaultNeo4jIsNewStrategy(idDescription, valueType, valueLookup);
|
||||
}
|
||||
|
||||
private final IdDescription idDescription;
|
||||
|
||||
private final Class<?> valueType;
|
||||
|
||||
private @Nullable 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()) {
|
||||
|
||||
boolean isNew = false;
|
||||
if (value != null && valueType.isPrimitive() && Number.class.isInstance(value)) {
|
||||
isNew = ((Number) value).longValue() < 0;
|
||||
} else {
|
||||
isNew = value == null;
|
||||
}
|
||||
|
||||
return isNew;
|
||||
} else if (idDescription.isExternallyGeneratedId()) {
|
||||
return value == null;
|
||||
} else if (idDescription.isAssignedId()) {
|
||||
if (valueType != null && !valueType.isPrimitive()) {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
if (Number.class.isInstance(value)) {
|
||||
return ((Number) value).longValue() == 0;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String
|
||||
.format("Could not determine whether %s is new! Unsupported identifier or version property!", entity));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static org.springframework.util.StringUtils.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.*;
|
||||
import org.neo4j.springframework.data.core.schema.GeneratedValue.InternalIdGenerator;
|
||||
import org.neo4j.springframework.data.core.schema.GeneratedValue.UUIDGenerator;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.support.IsNewStrategy;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
|
||||
implements Neo4jPersistentEntity<T> {
|
||||
|
||||
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Collections.unmodifiableSet(new HashSet<>(
|
||||
Arrays.asList(Long.class, long.class)));
|
||||
|
||||
/**
|
||||
* If an entity is annotated with {@link Node}, we consider this as an explicit entity
|
||||
* that should get validated more strictly.
|
||||
*/
|
||||
private final Boolean isExplicitEntity;
|
||||
|
||||
/**
|
||||
* The label that describes the label most concrete.
|
||||
*/
|
||||
private final String primaryLabel;
|
||||
|
||||
private final Lazy<List<String>> additionalLabels;
|
||||
|
||||
/**
|
||||
* Projections need to be also be eligible entities but don't define id fields.
|
||||
*/
|
||||
@Nullable
|
||||
private IdDescription idDescription;
|
||||
|
||||
private final Lazy<Collection<GraphPropertyDescription>> graphProperties;
|
||||
|
||||
private final Set<NodeDescription<?>> childNodeDescriptions = new HashSet<>();
|
||||
|
||||
private NodeDescription<?> parentNodeDescription;
|
||||
|
||||
private final Lazy<Neo4jPersistentProperty> dynamicLabelsProperty;
|
||||
|
||||
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
|
||||
super(information);
|
||||
|
||||
this.isExplicitEntity = this.isAnnotationPresent(Node.class);
|
||||
this.primaryLabel = computePrimaryLabel();
|
||||
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));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getPrimaryLabel()
|
||||
*/
|
||||
@Override
|
||||
public String getPrimaryLabel() {
|
||||
return primaryLabel;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getUnderlyingClass()
|
||||
*/
|
||||
@Override
|
||||
public Class<T> getUnderlyingClass() {
|
||||
return getType();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getIdDescription()
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public IdDescription getIdDescription() {
|
||||
return this.idDescription;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getGraphProperties()
|
||||
*/
|
||||
@Override
|
||||
public Collection<GraphPropertyDescription> getGraphProperties() {
|
||||
return this.graphProperties.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAdditionalLabels() {
|
||||
return this.additionalLabels.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see NodeDescription#getGraphProperty(String)
|
||||
*/
|
||||
@Override
|
||||
public Optional<GraphPropertyDescription> getGraphProperty(String fieldName) {
|
||||
return Optional.ofNullable(this.getPersistentProperty(fieldName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Neo4jPersistentProperty> getDynamicLabelsProperty() {
|
||||
return this.dynamicLabelsProperty.getOptional();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see BasicPersistentEntity#getFallbackIsNewStrategy()
|
||||
*/
|
||||
@Override
|
||||
protected IsNewStrategy getFallbackIsNewStrategy() {
|
||||
return DefaultNeo4jIsNewStrategy.basedOn(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify() {
|
||||
|
||||
super.verify();
|
||||
|
||||
this.idDescription = computeIdDescription();
|
||||
|
||||
verifyNoDuplicatedGraphProperties();
|
||||
verifyDynamicAssociations();
|
||||
verifyDynamicLabels();
|
||||
}
|
||||
|
||||
private void verifyNoDuplicatedGraphProperties() {
|
||||
|
||||
Set<String> seen = new HashSet<>();
|
||||
Set<String> duplicates = new HashSet<>();
|
||||
this.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) persistentProperty -> {
|
||||
String propertyName = persistentProperty.getPropertyName();
|
||||
if (seen.contains(propertyName)) {
|
||||
duplicates.add(propertyName);
|
||||
} 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()));
|
||||
}
|
||||
|
||||
private void verifyDynamicAssociations() {
|
||||
|
||||
Set<Class> targetEntities = new HashSet<>();
|
||||
this.doWithAssociations((Association<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 = "
|
||||
+ relationship.direction().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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void verifyDynamicLabels() {
|
||||
|
||||
Set<String> namesOfPropertiesWithDynamicLabels = new HashSet<>();
|
||||
|
||||
this.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) persistentProperty -> {
|
||||
if (!persistentProperty.isAnnotationPresent(DynamicLabels.class)) {
|
||||
return;
|
||||
}
|
||||
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(namesOfPropertiesWithDynamicLabels.size() <= 1, () ->
|
||||
String.format(
|
||||
"Multiple properties in entity %s are annotated with @%s: %s.", getUnderlyingClass(),
|
||||
DynamicLabels.class.getSimpleName(), namesOfPropertiesWithDynamicLabels));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return computed primary label
|
||||
*/
|
||||
private String computePrimaryLabel() {
|
||||
|
||||
Node nodeAnnotation = this.findAnnotation(Node.class);
|
||||
if (nodeAnnotation == null || hasEmptyLabelInformation(nodeAnnotation)) {
|
||||
return this.getType().getSimpleName();
|
||||
} else if (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.
|
||||
*
|
||||
* @return all additional labels.
|
||||
*/
|
||||
private List<String> computeAdditionalLabels() {
|
||||
|
||||
return Stream.concat(computeOwnAdditionalLabels().stream(), computeParentLabels().stream())
|
||||
.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>
|
||||
* 3. If only {@link Node#labels()} property is set, use the all but the first one as the additional labels.<br>
|
||||
* 3. If the {@link Node#primaryLabel()} property is set, use the all but the first one as the additional labels.<br>
|
||||
*
|
||||
* @return computed additional labels of the concrete class
|
||||
*/
|
||||
@NonNull
|
||||
private List<String> computeOwnAdditionalLabels() {
|
||||
Node nodeAnnotation = this.findAnnotation(Node.class);
|
||||
if (nodeAnnotation == null || hasEmptyLabelInformation(nodeAnnotation)) {
|
||||
return emptyList();
|
||||
} else if (hasText(nodeAnnotation.primaryLabel())) {
|
||||
return Arrays.asList(nodeAnnotation.labels());
|
||||
} else {
|
||||
return Arrays.asList(Arrays.copyOfRange(nodeAnnotation.labels(), 1, nodeAnnotation.labels().length));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<String> computeParentLabels() {
|
||||
|
||||
List<String> parentLabels = new ArrayList<>();
|
||||
while (parentNodeDescription != null) {
|
||||
parentLabels.add(parentNodeDescription.getPrimaryLabel());
|
||||
parentLabels.addAll(parentNodeDescription.getAdditionalLabels());
|
||||
parentNodeDescription = ((DefaultNeo4jPersistentEntity<?>) parentNodeDescription).getParentNodeDescription();
|
||||
}
|
||||
return parentLabels;
|
||||
}
|
||||
|
||||
private static boolean hasEmptyLabelInformation(Node nodeAnnotation) {
|
||||
return nodeAnnotation.labels().length < 1 && !hasText(nodeAnnotation.primaryLabel());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private IdDescription computeIdDescription() {
|
||||
|
||||
Neo4jPersistentProperty idProperty = this.getIdProperty();
|
||||
if (idProperty == null && isExplicitEntity) {
|
||||
throw new IllegalStateException("Missing id property on " + this.getUnderlyingClass() + ".");
|
||||
} else if (idProperty == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GeneratedValue generatedValueAnnotation = idProperty.findAnnotation(GeneratedValue.class);
|
||||
|
||||
String propertyName = idProperty.getPropertyName();
|
||||
|
||||
// Assigned ids
|
||||
if (generatedValueAnnotation == null) {
|
||||
return IdDescription.forAssignedIds(propertyName);
|
||||
}
|
||||
|
||||
Class<? extends IdGenerator<?>> idGeneratorClass = generatedValueAnnotation.generatorClass();
|
||||
String idGeneratorRef = generatedValueAnnotation.generatorRef();
|
||||
|
||||
if (idProperty.getActualType() == UUID.class && idGeneratorClass == InternalIdGenerator.class
|
||||
&& !StringUtils.hasText(idGeneratorRef)) {
|
||||
idGeneratorClass = UUIDGenerator.class;
|
||||
}
|
||||
|
||||
// Internally generated ids.
|
||||
if (idGeneratorClass == 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());
|
||||
}
|
||||
|
||||
if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES);
|
||||
}
|
||||
|
||||
return IdDescription.forInternallyGeneratedIds();
|
||||
}
|
||||
|
||||
// Externally generated ids.
|
||||
return IdDescription.forExternallyGeneratedIds(idGeneratorClass, idGeneratorRef, propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RelationshipDescription> getRelationships() {
|
||||
|
||||
final List<RelationshipDescription> relationships = new ArrayList<>();
|
||||
this.doWithAssociations((Association<Neo4jPersistentProperty> association) ->
|
||||
relationships.add((RelationshipDescription) association)
|
||||
);
|
||||
return Collections.unmodifiableCollection(relationships);
|
||||
}
|
||||
|
||||
private Collection<GraphPropertyDescription> computeGraphProperties() {
|
||||
|
||||
final List<GraphPropertyDescription> computedGraphProperties = new ArrayList<>();
|
||||
|
||||
doWithProperties((PropertyHandler<Neo4jPersistentProperty>) computedGraphProperties::add);
|
||||
|
||||
return Collections.unmodifiableCollection(computedGraphProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy() {
|
||||
|
||||
TreeSet<GraphPropertyDescription> allPropertiesInHierarchy =
|
||||
new TreeSet<>(Comparator.comparing(GraphPropertyDescription::getPropertyName));
|
||||
|
||||
allPropertiesInHierarchy.addAll(getGraphProperties());
|
||||
for (NodeDescription<?> childNodeDescription : getChildNodeDescriptionsInHierarchy()) {
|
||||
Collection<GraphPropertyDescription> childGraphProperties = childNodeDescription.getGraphProperties();
|
||||
allPropertiesInHierarchy.addAll(childGraphProperties);
|
||||
}
|
||||
|
||||
return allPropertiesInHierarchy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChildNodeDescription(NodeDescription<?> child) {
|
||||
this.childNodeDescriptions.add(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<NodeDescription<?>> getChildNodeDescriptionsInHierarchy() {
|
||||
Set<NodeDescription<?>> childNodes = new HashSet<>(childNodeDescriptions);
|
||||
|
||||
for (NodeDescription<?> childNodeDescription : childNodeDescriptions) {
|
||||
childNodes.addAll(childNodeDescription.getChildNodeDescriptionsInHierarchy());
|
||||
}
|
||||
return childNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParentNodeDescription(NodeDescription<?> parent) {
|
||||
this.parentNodeDescription = parent;
|
||||
}
|
||||
|
||||
private NodeDescription<?> getParentNodeDescription() {
|
||||
return parentNodeDescription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.Relationship;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipProperties;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
|
||||
import org.springframework.data.mapping.model.Property;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<Neo4jPersistentProperty>
|
||||
implements Neo4jPersistentProperty {
|
||||
|
||||
private final Lazy<String> graphPropertyName;
|
||||
private final Lazy<Boolean> isAssociation;
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AnnotationBasedPersistentProperty}.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param owner must not be {@literal null}.
|
||||
* @param simpleTypeHolder type holder
|
||||
*/
|
||||
DefaultNeo4jPersistentProperty(Property property,
|
||||
PersistentEntity<?, Neo4jPersistentProperty> owner,
|
||||
Neo4jMappingContext mappingContext,
|
||||
SimpleTypeHolder simpleTypeHolder) {
|
||||
|
||||
super(property, owner, simpleTypeHolder);
|
||||
|
||||
this.graphPropertyName = Lazy.of(this::computeGraphPropertyName);
|
||||
this.isAssociation = Lazy.of(() -> {
|
||||
|
||||
Class<?> targetType = getActualType();
|
||||
return !(simpleTypeHolder.isSimpleType(targetType) || mappingContext.hasCustomWriteTarget(targetType));
|
||||
});
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Association<Neo4jPersistentProperty> createAssociation() {
|
||||
|
||||
Neo4jPersistentEntity<?> obverseOwner;
|
||||
|
||||
// if the target is a relationship property always take the key type from the map instead of the value type.
|
||||
if (this.hasActualTypeAnnotation(RelationshipProperties.class)) {
|
||||
obverseOwner = this.mappingContext.getPersistentEntity(this.getComponentType());
|
||||
} else {
|
||||
obverseOwner = this.mappingContext.getPersistentEntity(this.getAssociationTargetType());
|
||||
}
|
||||
|
||||
Relationship outgoingRelationship = this.findAnnotation(Relationship.class);
|
||||
|
||||
String type;
|
||||
if (outgoingRelationship != null && outgoingRelationship.type() != null) {
|
||||
type = outgoingRelationship.type();
|
||||
} else {
|
||||
type = deriveRelationshipType(this.getName());
|
||||
}
|
||||
|
||||
Relationship.Direction direction = Relationship.Direction.OUTGOING;
|
||||
if (outgoingRelationship != null) {
|
||||
direction = outgoingRelationship.direction();
|
||||
}
|
||||
|
||||
boolean dynamicAssociation = this.isDynamicAssociation();
|
||||
|
||||
// Because a dynamic association is also represented as a Map, this ensures that the
|
||||
// relationship properties class will only have a value if it's not a dynamic association.
|
||||
Class<?> relationshipPropertiesClass = dynamicAssociation ? null : getMapValueType();
|
||||
|
||||
// Try to determine if there is a relationship definition that expresses logically the same relationship
|
||||
// on the other end.
|
||||
Optional<RelationshipDescription> obverseRelationshipDescription = obverseOwner.getRelationships().stream()
|
||||
.filter(rel -> rel.getType().equals(type) && rel.getTarget().equals(this.getOwner()))
|
||||
.findFirst();
|
||||
|
||||
DefaultRelationshipDescription relationshipDescription = new DefaultRelationshipDescription(this,
|
||||
obverseRelationshipDescription.orElse(null), type, dynamicAssociation, (NodeDescription<?>) getOwner(),
|
||||
this.getName(), obverseOwner, direction, relationshipPropertiesClass);
|
||||
|
||||
// Update the previous found, if any, relationship with the newly created one as its counterpart.
|
||||
obverseRelationshipDescription
|
||||
.ifPresent(relationship -> relationship.setRelationshipObverse(relationshipDescription));
|
||||
|
||||
return relationshipDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getAssociationTargetType() {
|
||||
|
||||
Class<?> associationTargetType = super.getAssociationTargetType();
|
||||
if (associationTargetType != null) {
|
||||
return associationTargetType;
|
||||
} else if (isDynamicOneToManyAssociation()) {
|
||||
TypeInformation<?> actualType = getTypeInformation().getRequiredActualType();
|
||||
return actualType.getRequiredComponentType().getType();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAssociation() {
|
||||
|
||||
return this.isAssociation.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
return super.isEntity() && isAssociation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the target name of this property.
|
||||
*
|
||||
* @return A property on a node or {@literal null} if this property describes an association.
|
||||
*/
|
||||
@Nullable
|
||||
private String computeGraphPropertyName() {
|
||||
|
||||
if (this.isAssociation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
org.neo4j.springframework.data.core.schema.Property propertyAnnotation =
|
||||
this.findAnnotation(org.neo4j.springframework.data.core.schema.Property.class);
|
||||
|
||||
String targetName = this.getName();
|
||||
if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty()
|
||||
&& propertyAnnotation.name().trim().length() != 0) {
|
||||
targetName = propertyAnnotation.name().trim();
|
||||
}
|
||||
|
||||
return targetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFieldName() {
|
||||
return this.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPropertyName() {
|
||||
|
||||
String propertyName = this.graphPropertyName.getNullable();
|
||||
if (propertyName == null) {
|
||||
throw new MappingException("This property is not mapped to a Graph property!");
|
||||
}
|
||||
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternalIdProperty() {
|
||||
|
||||
return this.isIdProperty() && ((Neo4jPersistentEntity) this.getOwner()).isUsingInternalIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationship() {
|
||||
|
||||
return isAssociation();
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.Relationship;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty> implements RelationshipDescription {
|
||||
|
||||
private final String type;
|
||||
|
||||
private final boolean dynamic;
|
||||
|
||||
private final NodeDescription<?> source;
|
||||
|
||||
private final NodeDescription<?> target;
|
||||
|
||||
private final String fieldName;
|
||||
|
||||
private final Relationship.Direction direction;
|
||||
|
||||
private Class<?> relationshipPropertiesClass;
|
||||
|
||||
private RelationshipDescription relationshipObverse;
|
||||
|
||||
DefaultRelationshipDescription(Neo4jPersistentProperty inverse,
|
||||
@Nullable RelationshipDescription relationshipObverse,
|
||||
String type, boolean dynamic, NodeDescription<?> source, String fieldName, NodeDescription<?> target,
|
||||
Relationship.Direction direction, @Nullable Class<?> relationshipPropertiesClass) {
|
||||
|
||||
// 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);
|
||||
|
||||
this.relationshipObverse = relationshipObverse;
|
||||
this.type = type;
|
||||
this.dynamic = dynamic;
|
||||
this.source = source;
|
||||
this.fieldName = fieldName;
|
||||
this.target = target;
|
||||
this.direction = direction;
|
||||
this.relationshipPropertiesClass = relationshipPropertiesClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeDescription<?> getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeDescription<?> getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFieldName() {
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Relationship.Direction getDirection() {
|
||||
return direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getRelationshipPropertiesClass() {
|
||||
return relationshipPropertiesClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRelationshipProperties() {
|
||||
return getRelationshipPropertiesClass() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRelationshipObverse(RelationshipDescription relationshipObverse) {
|
||||
this.relationshipObverse = relationshipObverse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelationshipDescription getRelationshipObverse() {
|
||||
return relationshipObverse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRelationshipObverse() {
|
||||
return this.relationshipObverse != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultRelationshipDescription{" +
|
||||
"type='" + type + '\'' +
|
||||
", source='" + source + '\'' +
|
||||
", direction='" + direction + '\'' +
|
||||
", target='" + target +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof DefaultRelationshipDescription)) {
|
||||
return false;
|
||||
}
|
||||
DefaultRelationshipDescription that = (DefaultRelationshipDescription) o;
|
||||
return getType().equals(that.getType()) && getTarget().equals(that.getTarget())
|
||||
&& getSource().equals(that.getSource()) && getDirection().equals(that.getDirection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, target, source, direction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jSimpleTypes;
|
||||
import org.neo4j.springframework.data.core.schema.IdGenerator;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.Schema;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.context.AbstractMappingContext;
|
||||
import org.springframework.data.mapping.model.Property;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* An implementation of both a {@link Schema} as well as a Neo4j version of Spring Data's
|
||||
* {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to provide
|
||||
* the initial set of classes through {@link #setInitialEntitySet(Set)}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class Neo4jMappingContext
|
||||
extends AbstractMappingContext<Neo4jPersistentEntity<?>, Neo4jPersistentProperty> implements Schema {
|
||||
|
||||
/**
|
||||
* A map of fallback id generators, that have not been added to the application context
|
||||
*/
|
||||
private final Map<Class<? extends IdGenerator<?>>, IdGenerator<?>> idGenerators = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break the dependency
|
||||
* cycle between this class and the {@link DefaultNeo4jConverter}.
|
||||
*/
|
||||
private final NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore();
|
||||
|
||||
/**
|
||||
* The converter used in this mapping context.
|
||||
*/
|
||||
private final Neo4jConverter converter;
|
||||
|
||||
private final Neo4jConversions neo4jConversions;
|
||||
|
||||
private @Nullable AutowireCapableBeanFactory beanFactory;
|
||||
|
||||
public Neo4jMappingContext() {
|
||||
|
||||
this(new Neo4jConversions());
|
||||
}
|
||||
|
||||
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
|
||||
|
||||
super.setSimpleTypeHolder(Neo4jSimpleTypes.HOLDER);
|
||||
this.neo4jConversions = neo4jConversions;
|
||||
this.converter = new DefaultNeo4jConverter(neo4jConversions, nodeDescriptionStore);
|
||||
}
|
||||
|
||||
public Neo4jConverter getConverter() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
boolean hasCustomWriteTarget(Class<?> targetType) {
|
||||
return neo4jConversions.hasCustomWriteTarget(targetType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)
|
||||
*/
|
||||
@Override
|
||||
protected <T> Neo4jPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
|
||||
final DefaultNeo4jPersistentEntity<T> newEntity = new DefaultNeo4jPersistentEntity<>(typeInformation);
|
||||
String primaryLabel = newEntity.getPrimaryLabel();
|
||||
|
||||
if (this.nodeDescriptionStore.containsKey(primaryLabel)) {
|
||||
// @formatter:off
|
||||
throw new MappingException(
|
||||
String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s",
|
||||
primaryLabel));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
if (this.nodeDescriptionStore.containsValue(newEntity)) {
|
||||
Optional<String> label = this.nodeDescriptionStore.entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(newEntity)).map(
|
||||
Map.Entry::getKey).findFirst();
|
||||
|
||||
throw new MappingException(
|
||||
String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s",
|
||||
newEntity, label.orElse("n/a")));
|
||||
}
|
||||
|
||||
NodeDescription<?> existingDescription = this.getNodeDescription(newEntity.getUnderlyingClass());
|
||||
if (existingDescription != null) {
|
||||
|
||||
throw new MappingException(String.format(Locale.ENGLISH,
|
||||
"The schema already contains description with the underlying class %s under the primary label %s",
|
||||
newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel()));
|
||||
}
|
||||
|
||||
this.nodeDescriptionStore.put(primaryLabel, newEntity);
|
||||
|
||||
// determine super class to create the node hierarchy
|
||||
Class<? super T> superclass = typeInformation.getType().getSuperclass();
|
||||
|
||||
if (isValidParentNode(superclass)) {
|
||||
Neo4jPersistentEntity<?> parentNodeDescription = getPersistentEntity(superclass);
|
||||
if (parentNodeDescription != null) {
|
||||
parentNodeDescription.addChildNodeDescription(newEntity);
|
||||
newEntity.setParentNodeDescription(parentNodeDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
private boolean isValidParentNode(@Nullable Class<?> parentClass) {
|
||||
if (parentClass == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isExplicitNode = parentClass.isAnnotationPresent(Node.class);
|
||||
boolean isAbstractClass = Modifier.isAbstract(parentClass.getModifiers());
|
||||
|
||||
return isExplicitNode && isAbstractClass;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder)
|
||||
*/
|
||||
@Override
|
||||
protected Neo4jPersistentProperty createPersistentProperty(Property property,
|
||||
Neo4jPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
|
||||
|
||||
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public NodeDescription<?> getNodeDescription(String primaryLabel) {
|
||||
return this.nodeDescriptionStore.get(primaryLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeDescription<?> getNodeDescription(Class<?> underlyingClass) {
|
||||
return this.nodeDescriptionStore.getNodeDescription(underlyingClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Neo4jPersistentEntity<?>> addPersistentEntity(Class<?> type) {
|
||||
return super.addPersistentEntity(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IdGenerator<?>> T getOrCreateIdGeneratorOfType(Class<T> idGeneratorType) {
|
||||
|
||||
if (this.idGenerators.containsKey(idGeneratorType)) {
|
||||
return (T) this.idGenerators.get(idGeneratorType);
|
||||
} else {
|
||||
T idGenerator;
|
||||
if (this.beanFactory == null) {
|
||||
idGenerator = BeanUtils.instantiateClass(idGeneratorType);
|
||||
} else {
|
||||
idGenerator = this.beanFactory.getBeanProvider(idGeneratorType)
|
||||
.getIfUnique(() -> this.beanFactory.createBean(idGeneratorType));
|
||||
}
|
||||
this.idGenerators.put(idGeneratorType, idGenerator);
|
||||
return idGenerator;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IdGenerator<?>> Optional<T> getIdGenerator(String reference) {
|
||||
try {
|
||||
return Optional.of((T) this.beanFactory.getBean(reference));
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
super.setApplicationContext(applicationContext);
|
||||
|
||||
this.beanFactory = applicationContext.getAutowireCapableBeanFactory();
|
||||
Driver driver = this.beanFactory.getBean(Driver.class);
|
||||
((DefaultNeo4jConverter) this.converter).setTypeSystem(driver.defaultTypeSystem());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.MutablePersistentEntity;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.data.mapping.PersistentEntity} interface with additional methods for metadata related to Neo4j.
|
||||
*
|
||||
* Both Spring Data methods {@link #doWithProperties(PropertyHandler)} and {@link #doWithAssociations(AssociationHandler)} are
|
||||
* aware which field of a class is meant to be mapped as a property of a node or a relationship or if it is a relationship
|
||||
* (in Spring Data terms: if it is an association).
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @param <T> type of the underlying class
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface Neo4jPersistentEntity<T>
|
||||
extends MutablePersistentEntity<T, Neo4jPersistentProperty>, NodeDescription<T> {
|
||||
|
||||
/**
|
||||
* @return An optional property pointing to a {@link java.util.Collection Collection<String>} containing dynamic "runtime managed" labels.
|
||||
*/
|
||||
Optional<Neo4jPersistentProperty> getDynamicLabelsProperty();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.schema.DynamicLabels;
|
||||
import org.neo4j.springframework.data.core.schema.GraphPropertyDescription;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipProperties;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related to Neo4j.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Philipp Tölle
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface Neo4jPersistentProperty
|
||||
extends PersistentProperty<Neo4jPersistentProperty>, GraphPropertyDescription {
|
||||
|
||||
/**
|
||||
* Dynamic associations are associations to non-simple types stored in a map
|
||||
* with a key type of {@literal java.lang.String} or enum.
|
||||
*
|
||||
* @return True, if this association is a dynamic association.
|
||||
*/
|
||||
default boolean isDynamicAssociation() {
|
||||
return isAssociation() && isMap() && (getComponentType() == String.class || getComponentType().isEnum());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic one-to-many associations are associations to non-simple types stored in a map
|
||||
* with a key type of {@literal java.lang.String} and values of {@literal java.util.Collection}.
|
||||
*
|
||||
* @return True, if this association is a dynamic association with multple values per type.
|
||||
* @since 1.0.1
|
||||
*/
|
||||
default boolean isDynamicOneToManyAssociation() {
|
||||
|
||||
return this.isDynamicAssociation() && getTypeInformation().getRequiredActualType().isCollectionLike();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the property is an property describing dynamic labels
|
||||
* @since 1.1
|
||||
*/
|
||||
default boolean isDynamicLabels() {
|
||||
return this.isAnnotationPresent(DynamicLabels.class) && this.isCollectionLike();
|
||||
}
|
||||
|
||||
/**
|
||||
* see if the association has a property class
|
||||
*
|
||||
* @return True, if this association has properties
|
||||
*/
|
||||
default boolean isRelationshipWithProperties() {
|
||||
return isAssociation()
|
||||
&& isMap()
|
||||
&& getMapValueType() != null
|
||||
&& getMapValueType().isAnnotationPresent(RelationshipProperties.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* Wraps a resolved node description together with the complete list of labels returned from the database and the list
|
||||
* of labels not statically defined in the resolved node hierarchy.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.1
|
||||
* @soundtrack The Rolling Stones - Living In A Ghost Town
|
||||
*/
|
||||
final class NodeDescriptionAndLabels {
|
||||
|
||||
private final NodeDescription<?> nodeDescription;
|
||||
|
||||
private final Collection<String> dynamicLabels;
|
||||
|
||||
NodeDescriptionAndLabels(NodeDescription<?> nodeDescription,
|
||||
Collection<String> dynamicLabels) {
|
||||
this.nodeDescription = nodeDescription;
|
||||
this.dynamicLabels = dynamicLabels;
|
||||
}
|
||||
|
||||
public NodeDescription<?> getNodeDescription() {
|
||||
return nodeDescription;
|
||||
}
|
||||
|
||||
public Collection<String> getDynamicLabels() {
|
||||
return dynamicLabels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.springframework.data.mapping.context.AbstractMappingContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* This class is more or less just a wrapper around the node description lookup map.
|
||||
* It ensures that there is no cyclic dependency between {@link Neo4jMappingContext} and {@link DefaultNeo4jConverter}.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
class NodeDescriptionStore {
|
||||
|
||||
/**
|
||||
* A lookup of entities based on their primary label. We depend on the locking mechanism provided by the
|
||||
* {@link AbstractMappingContext}, so this lookup is not synchronized further.
|
||||
*/
|
||||
private final Map<String, NodeDescription<?>> nodeDescriptionsByPrimaryLabel = new HashMap<>();
|
||||
|
||||
public boolean containsKey(String primaryLabel) {
|
||||
return nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel);
|
||||
}
|
||||
|
||||
public <T> boolean containsValue(DefaultNeo4jPersistentEntity<T> newEntity) {
|
||||
return nodeDescriptionsByPrimaryLabel.containsValue(newEntity);
|
||||
}
|
||||
|
||||
public <T> void put(String primaryLabel, DefaultNeo4jPersistentEntity<T> newEntity) {
|
||||
nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity);
|
||||
}
|
||||
|
||||
public Set<Map.Entry<String, NodeDescription<?>>> entrySet() {
|
||||
return nodeDescriptionsByPrimaryLabel.entrySet();
|
||||
}
|
||||
|
||||
public Collection<NodeDescription<?>> values() {
|
||||
return nodeDescriptionsByPrimaryLabel.values();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NodeDescription<?> get(String primaryLabel) {
|
||||
return nodeDescriptionsByPrimaryLabel.get(primaryLabel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NodeDescription<?> getNodeDescription(Class<?> targetType) {
|
||||
for (NodeDescription<?> nodeDescription : values()) {
|
||||
if (nodeDescription.getUnderlyingClass().equals(targetType)) {
|
||||
return nodeDescription;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public NodeDescriptionAndLabels deriveConcreteNodeDescription(
|
||||
Neo4jPersistentEntity<?> entityDescription,
|
||||
List<String> labels
|
||||
) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return new NodeDescriptionAndLabels(entityDescription, Collections.emptyList());
|
||||
}
|
||||
for (NodeDescription<?> childNodeDescription : entityDescription.getChildNodeDescriptionsInHierarchy()) {
|
||||
String primaryLabel = childNodeDescription.getPrimaryLabel();
|
||||
List<String> additionalLabels = new ArrayList<>(childNodeDescription.getAdditionalLabels());
|
||||
additionalLabels.add(primaryLabel);
|
||||
if (additionalLabels.containsAll(labels)) {
|
||||
Set<String> surplusLabels = new HashSet<>(labels);
|
||||
surplusLabels.remove(primaryLabel);
|
||||
surplusLabels.removeAll(additionalLabels);
|
||||
return new NodeDescriptionAndLabels(childNodeDescription, surplusLabels);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> surplusLabels = new HashSet<>(labels);
|
||||
surplusLabels.remove(entityDescription.getPrimaryLabel());
|
||||
surplusLabels.removeAll(entityDescription.getAdditionalLabels());
|
||||
return new NodeDescriptionAndLabels(entityDescription, surplusLabels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* The main mapping framework. This package contains all the public facing annotations necessary to mark Spring Data Neo4j entities.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This package contains the core infrastructure for creating a imperative or reactive client that can execute
|
||||
* queries.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import static org.apiguardian.api.API.Status.*;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.SymbolicName;
|
||||
|
||||
/**
|
||||
* A pool of constants used in our Cypher generation. These constants may change without further notice.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Milky Chance - Sadnecessary
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = INTERNAL, since = "1.0")
|
||||
public final class Constants {
|
||||
|
||||
public static final SymbolicName NAME_OF_ROOT_NODE = Cypher.name("n");
|
||||
|
||||
public static final String NAME_OF_INTERNAL_ID = "__internalNeo4jId__";
|
||||
public static final String NAME_OF_LABELS = "__nodeLabels__";
|
||||
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_STATIC_LABELS_PARAM = "__staticLabels__";
|
||||
public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__";
|
||||
|
||||
public static final String FROM_ID_PARAMETER_NAME = "fromId";
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import static org.neo4j.cypherdsl.core.Cypher.*;
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
import static org.neo4j.springframework.data.core.schema.RelationshipDescription.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.Relationship;
|
||||
import org.neo4j.cypherdsl.core.*;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A generator based on the schema defined by node and relationship descriptions.
|
||||
* Most methods return renderable Cypher statements.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @author Philipp Tölle
|
||||
* @soundtrack Rammstein - Herzeleid
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public enum CypherGenerator {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final SymbolicName START_NODE_NAME = Cypher.name("startNode");
|
||||
private static final SymbolicName END_NODE_NAME = Cypher.name("endNode");
|
||||
|
||||
private static final SymbolicName RELATIONSHIP_NAME = Cypher.name("relProps");
|
||||
|
||||
private static final int RELATIONSHIP_DEPTH_LIMIT = 2;
|
||||
|
||||
/**
|
||||
* @param nodeDescription The node description for which a match clause should be generated
|
||||
* @return An ongoing match
|
||||
* @see #prepareMatchOf(NodeDescription, Condition)
|
||||
*/
|
||||
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription) {
|
||||
return prepareMatchOf(nodeDescription, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will create a match statement that fits the given node description and may contains additional conditions.
|
||||
* The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to
|
||||
* the given {@code nodeDescription}.
|
||||
* <p>
|
||||
* It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end.
|
||||
* <p>
|
||||
* The root node is guaranteed to have the symbolic name {@code n}.
|
||||
*
|
||||
* @param nodeDescription The node description for which a match clause should be generated
|
||||
* @param condition Optional conditions to add
|
||||
* @return An ongoing match
|
||||
*/
|
||||
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription, @Nullable
|
||||
Condition condition) {
|
||||
|
||||
String primaryLabel = nodeDescription.getPrimaryLabel();
|
||||
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
|
||||
|
||||
Node rootNode = node(primaryLabel, additionalLabels).named(NAME_OF_ROOT_NODE);
|
||||
IdDescription idDescription = nodeDescription.getIdDescription();
|
||||
|
||||
List<Expression> expressions = new ArrayList<>();
|
||||
expressions.add(NAME_OF_ROOT_NODE);
|
||||
if (idDescription.isInternallyGeneratedId()) {
|
||||
expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID));
|
||||
}
|
||||
return match(rootNode).where(conditionOrNoCondition(condition))
|
||||
.with(expressions.toArray(new Expression[] {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a statement that returns all labels of a node that are not part of a list parameter named {@link Constants#NAME_OF_STATIC_LABELS_PARAM}.
|
||||
* Those are the "dynamic labels" of a node as set through SDN/RX.
|
||||
*
|
||||
* @param nodeDescription The node description for which the statement should be generated
|
||||
* @return A statement having one parameter.
|
||||
* @since 1.1
|
||||
*/
|
||||
public Statement createStatementReturningDynamicLabels(NodeDescription<?> nodeDescription) {
|
||||
|
||||
final Node rootNode = anyNode(NAME_OF_ROOT_NODE);
|
||||
|
||||
Condition versionCondition;
|
||||
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
|
||||
|
||||
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty();
|
||||
versionCondition = rootNode.property(versionProperty.getName()).isEqualTo(parameter(NAME_OF_VERSION_PARAM));
|
||||
} else {
|
||||
versionCondition = Conditions.noCondition();
|
||||
}
|
||||
|
||||
return match(rootNode)
|
||||
.where(nodeDescription.getIdDescription().asIdExpression().isEqualTo(parameter(NAME_OF_ID)))
|
||||
.and(versionCondition)
|
||||
.unwind(rootNode.labels()).as("label")
|
||||
.with(Cypher.name("label")).where(Cypher.name("label").in(parameter(NAME_OF_STATIC_LABELS_PARAM)).not())
|
||||
.returning(Functions.collect(Cypher.name("label")).as(NAME_OF_LABELS)).build();
|
||||
}
|
||||
|
||||
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription) {
|
||||
return prepareDeleteOf(nodeDescription, null);
|
||||
}
|
||||
|
||||
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable Condition condition) {
|
||||
|
||||
Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
|
||||
.named(NAME_OF_ROOT_NODE);
|
||||
return match(rootNode).where(conditionOrNoCondition(condition)).detachDelete(rootNode).build();
|
||||
}
|
||||
|
||||
public Statement prepareSaveOf(NodeDescription<?> nodeDescription, UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
|
||||
|
||||
String primaryLabel = nodeDescription.getPrimaryLabel();
|
||||
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
|
||||
|
||||
Node rootNode = node(primaryLabel, additionalLabels).named(NAME_OF_ROOT_NODE);
|
||||
IdDescription idDescription = nodeDescription.getIdDescription();
|
||||
Parameter idParameter = parameter(NAME_OF_ID);
|
||||
|
||||
if (!idDescription.isInternallyGeneratedId()) {
|
||||
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
|
||||
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
|
||||
|
||||
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
|
||||
|
||||
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription)
|
||||
.getRequiredVersionProperty();
|
||||
String nameOfPossibleExistingNode = "hlp";
|
||||
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
|
||||
|
||||
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
|
||||
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.with(possibleExistingNode).where(possibleExistingNode.isNull())
|
||||
.create(rootNode)
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId())
|
||||
.build();
|
||||
|
||||
Statement updateIfExists = updateDecorator.apply(match(rootNode)
|
||||
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(NAME_OF_VERSION_PARAM)))
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId())
|
||||
.build();
|
||||
return Cypher.union(createIfNew, updateIfExists);
|
||||
|
||||
} else {
|
||||
return updateDecorator.apply(
|
||||
Cypher.merge(rootNode.withProperties(nameOfIdProperty, idParameter))
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM))
|
||||
).returning(rootNode.internalId()).build();
|
||||
}
|
||||
} else {
|
||||
String nameOfPossibleExistingNode = "hlp";
|
||||
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
|
||||
|
||||
Statement createIfNew;
|
||||
Statement updateIfExists;
|
||||
|
||||
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
|
||||
|
||||
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription)
|
||||
.getRequiredVersionProperty();
|
||||
|
||||
createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
|
||||
.where(possibleExistingNode.internalId().isEqualTo(idParameter))
|
||||
.with(possibleExistingNode).where(possibleExistingNode.isNull())
|
||||
.create(rootNode)
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId()).build();
|
||||
|
||||
updateIfExists = updateDecorator.apply(match(rootNode)
|
||||
.where(rootNode.internalId().isEqualTo(idParameter))
|
||||
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(NAME_OF_VERSION_PARAM)))
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId())
|
||||
.build();
|
||||
} else {
|
||||
createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
|
||||
.where(possibleExistingNode.internalId().isEqualTo(idParameter))
|
||||
.with(possibleExistingNode).where(possibleExistingNode.isNull())
|
||||
.create(rootNode)
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId())
|
||||
.build();
|
||||
|
||||
updateIfExists = updateDecorator.apply(match(rootNode)
|
||||
.where(rootNode.internalId().isEqualTo(idParameter))
|
||||
.set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode.internalId()).build();
|
||||
}
|
||||
|
||||
return Cypher.union(createIfNew, updateIfExists);
|
||||
}
|
||||
}
|
||||
|
||||
public Statement prepareSaveOfMultipleInstancesOf(NodeDescription<?> nodeDescription) {
|
||||
|
||||
Assert.isTrue(!nodeDescription.isUsingInternalIds(),
|
||||
"Only entities that use external IDs can be saved in a batch.");
|
||||
|
||||
Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
|
||||
.named(NAME_OF_ROOT_NODE);
|
||||
IdDescription idDescription = nodeDescription.getIdDescription();
|
||||
|
||||
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
|
||||
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
|
||||
|
||||
String row = "entity";
|
||||
return Cypher
|
||||
.unwind(parameter(NAME_OF_ENTITY_LIST_PARAM)).as(row)
|
||||
.merge(rootNode.withProperties(nameOfIdProperty, property(row, NAME_OF_ID)))
|
||||
.set(rootNode, property(row, NAME_OF_PROPERTIES_PARAM))
|
||||
.returning(Functions.collect(rootNode.property(nameOfIdProperty)).as(NAME_OF_IDS))
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Statement createRelationshipCreationQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
RelationshipDescription relationship, @Nullable String dynamicRelationshipType, Long relatedInternalId) {
|
||||
final Node startNode = neo4jPersistentEntity.isUsingInternalIds()
|
||||
? anyNode(START_NODE_NAME)
|
||||
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
|
||||
.named(START_NODE_NAME);
|
||||
|
||||
final Node endNode = anyNode(END_NODE_NAME);
|
||||
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
|
||||
|
||||
Parameter idParameter = parameter(FROM_ID_PARAMETER_NAME);
|
||||
String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType();
|
||||
return match(startNode)
|
||||
.where(neo4jPersistentEntity.isUsingInternalIds()
|
||||
? startNode.internalId().isEqualTo(idParameter)
|
||||
: startNode.property(idPropertyName).isEqualTo(idParameter))
|
||||
.match(endNode)
|
||||
.where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
|
||||
.merge(relationship.isOutgoing()
|
||||
? startNode.relationshipTo(endNode, type)
|
||||
: startNode.relationshipFrom(endNode, type)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Statement createRelationshipWithPropertiesCreationQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
RelationshipDescription relationship, Long relatedInternalId) {
|
||||
|
||||
Assert.isTrue(relationship.hasRelationshipProperties(),
|
||||
"Properties required to create a relationship with properties");
|
||||
Assert.isTrue(!relationship.isDynamic(),
|
||||
"Creation of relationships with properties is only supported for non-dynamic relationships");
|
||||
|
||||
Node startNode = anyNode(START_NODE_NAME);
|
||||
Node endNode = anyNode(END_NODE_NAME);
|
||||
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
|
||||
|
||||
Parameter idParameter = parameter(FROM_ID_PARAMETER_NAME);
|
||||
Parameter relationshipProperties = parameter(NAME_OF_PROPERTIES_PARAM);
|
||||
String type = relationship.getType();
|
||||
|
||||
Relationship relOutgoing = startNode.relationshipTo(endNode, type).named(RELATIONSHIP_NAME);
|
||||
Relationship relIncoming = startNode.relationshipFrom(endNode, type).named(RELATIONSHIP_NAME);
|
||||
|
||||
return match(startNode)
|
||||
.where(neo4jPersistentEntity.isUsingInternalIds()
|
||||
? startNode.internalId().isEqualTo(idParameter)
|
||||
: startNode.property(idPropertyName).isEqualTo(idParameter))
|
||||
.match(endNode)
|
||||
.where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
|
||||
.merge(relationship.isOutgoing()
|
||||
? relOutgoing
|
||||
: relIncoming
|
||||
)
|
||||
.set(RELATIONSHIP_NAME, relationshipProperties)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Statement createRelationshipRemoveQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
|
||||
RelationshipDescription relationshipDescription, Neo4jPersistentEntity relatedNode) {
|
||||
final Node startNode = neo4jPersistentEntity.isUsingInternalIds()
|
||||
? anyNode(START_NODE_NAME)
|
||||
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
|
||||
.named(START_NODE_NAME);
|
||||
|
||||
final Node endNode = node(relatedNode.getPrimaryLabel(), relatedNode.getAdditionalLabels());
|
||||
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
|
||||
boolean outgoing = relationshipDescription.isOutgoing();
|
||||
|
||||
String relationshipType = relationshipDescription.isDynamic() ? null : relationshipDescription.getType();
|
||||
|
||||
String relationshipToRemoveName = "rel";
|
||||
Relationship relationship = outgoing
|
||||
? startNode.relationshipTo(endNode, relationshipType).named(relationshipToRemoveName)
|
||||
: startNode.relationshipFrom(endNode, relationshipType).named(relationshipToRemoveName);
|
||||
|
||||
Parameter idParameter = parameter(FROM_ID_PARAMETER_NAME);
|
||||
return match(relationship)
|
||||
.where(neo4jPersistentEntity.isUsingInternalIds()
|
||||
? startNode.internalId().isEqualTo(idParameter)
|
||||
: startNode.property(idPropertyName).isEqualTo(idParameter))
|
||||
.delete(relationship.getSymbolicName().get()).build();
|
||||
}
|
||||
|
||||
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription) {
|
||||
return createReturnStatementForMatch(nodeDescription, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nodeDescription Description of the root node
|
||||
* @param inputProperties A list of Java properties of the domain to be included.
|
||||
* Those properties are compared with the field names of graph properties respectively relationships.
|
||||
* @return An expresion to be returned by a Cypher statement
|
||||
*/
|
||||
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription,
|
||||
@Nullable List<String> inputProperties) {
|
||||
|
||||
Predicate<String> includeField = s -> inputProperties == null || inputProperties.isEmpty()
|
||||
|| inputProperties.contains(s);
|
||||
|
||||
List<RelationshipDescription> processedRelationships = new ArrayList<>();
|
||||
|
||||
return projectPropertiesAndRelationships(nodeDescription, NAME_OF_ROOT_NODE, includeField,
|
||||
processedRelationships);
|
||||
}
|
||||
|
||||
private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> nodeDescription,
|
||||
SymbolicName nodeName,
|
||||
List<RelationshipDescription> processedRelationships) {
|
||||
|
||||
Predicate<String> includeAllFields = (field) -> true;
|
||||
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeAllFields, processedRelationships);
|
||||
}
|
||||
|
||||
private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeDescription,
|
||||
SymbolicName nodeName,
|
||||
Predicate<String> includeProperty,
|
||||
List<RelationshipDescription> processedRelationships) {
|
||||
|
||||
List<Object> contentOfProjection = new ArrayList<>();
|
||||
contentOfProjection.addAll(projectNodeProperties(nodeDescription, nodeName, includeProperty));
|
||||
contentOfProjection.addAll(
|
||||
generateListsFor(nodeDescription.getRelationships(), nodeName, includeProperty, processedRelationships)
|
||||
);
|
||||
|
||||
return Cypher.anyNode(nodeName).project(contentOfProjection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of objects that represents a very basic of {@code MapEntry<String, Object>} with the exception that
|
||||
* this list can also contain two "keys" in a row. The {@link MapProjection} will take care to handle them as
|
||||
* self-reflecting fields. Example with self-reflection and explicit value: {@code n {.id, name: n.name}}.
|
||||
*/
|
||||
private List<Object> projectNodeProperties(NodeDescription<?> nodeDescription, SymbolicName nodeName,
|
||||
Predicate<String> includeField) {
|
||||
|
||||
List<Object> nodePropertiesProjection = new ArrayList<>();
|
||||
Node node = anyNode(nodeName);
|
||||
for (GraphPropertyDescription property : nodeDescription.getGraphPropertiesInHierarchy()) {
|
||||
if (!includeField.test(property.getFieldName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.isInternalIdProperty()) {
|
||||
nodePropertiesProjection.add(NAME_OF_INTERNAL_ID);
|
||||
nodePropertiesProjection.add(Functions.id(node));
|
||||
} else if (!((Neo4jPersistentProperty) property).isDynamicLabels()) {
|
||||
nodePropertiesProjection.add(property.getPropertyName());
|
||||
}
|
||||
}
|
||||
|
||||
nodePropertiesProjection.add(NAME_OF_LABELS);
|
||||
nodePropertiesProjection.add(Functions.labels(node));
|
||||
|
||||
return nodePropertiesProjection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.neo4j.springframework.data.core.schema.CypherGenerator#projectNodeProperties
|
||||
*/
|
||||
private List<Object> generateListsFor(Collection<RelationshipDescription> relationships,
|
||||
SymbolicName nodeName, Predicate<String> includeField,
|
||||
List<RelationshipDescription> processedRelationships) {
|
||||
|
||||
List<Object> mapProjectionLists = new ArrayList<>();
|
||||
|
||||
for (RelationshipDescription relationshipDescription : relationships) {
|
||||
|
||||
String fieldName = relationshipDescription.getFieldName();
|
||||
if (!includeField.test(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we already processed the other way before, do not try to jump in the infinite loop
|
||||
// unless it is a root node relationship
|
||||
if (!nodeName.equals(NAME_OF_ROOT_NODE) && relationshipDescription.hasRelationshipObverse()
|
||||
&& processedRelationships.contains(relationshipDescription.getRelationshipObverse())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Collections.frequency(processedRelationships, relationshipDescription) > RELATIONSHIP_DEPTH_LIMIT) {
|
||||
return mapProjectionLists;
|
||||
}
|
||||
|
||||
generateListFor(relationshipDescription, nodeName, processedRelationships, fieldName, mapProjectionLists);
|
||||
}
|
||||
|
||||
return mapProjectionLists;
|
||||
}
|
||||
|
||||
private void generateListFor(RelationshipDescription relationshipDescription, SymbolicName nodeName,
|
||||
List<RelationshipDescription> processedRelationships, String fieldName, List<Object> mapProjectionLists) {
|
||||
|
||||
String relationshipType = relationshipDescription.getType();
|
||||
String relationshipTargetName = relationshipDescription.generateRelatedNodesCollectionName();
|
||||
String targetPrimaryLabel = relationshipDescription.getTarget().getPrimaryLabel();
|
||||
List<String> targetAdditionalLabels = relationshipDescription.getTarget().getAdditionalLabels();
|
||||
|
||||
Node startNode = anyNode(nodeName);
|
||||
SymbolicName relationshipFieldName = nodeName.concat("_" + fieldName);
|
||||
Node endNode = node(targetPrimaryLabel, targetAdditionalLabels).named(relationshipFieldName);
|
||||
NodeDescription<?> endNodeDescription = relationshipDescription.getTarget();
|
||||
|
||||
processedRelationships.add(relationshipDescription);
|
||||
|
||||
if (relationshipDescription.isDynamic()) {
|
||||
Relationship relationship = relationshipDescription
|
||||
.isOutgoing()
|
||||
? startNode.relationshipTo(endNode)
|
||||
: startNode.relationshipFrom(endNode);
|
||||
relationship = relationship.named(relationshipTargetName);
|
||||
|
||||
addMapProjection(relationshipTargetName,
|
||||
listBasedOn(relationship)
|
||||
.returning(
|
||||
projectAllPropertiesAndRelationships(endNodeDescription,
|
||||
relationshipFieldName, new ArrayList<>(processedRelationships))
|
||||
.and(NAME_OF_RELATIONSHIP_TYPE, Functions.type(relationship))),
|
||||
mapProjectionLists);
|
||||
|
||||
} else {
|
||||
Relationship relationship = relationshipDescription.isOutgoing()
|
||||
? startNode.relationshipTo(endNode, relationshipType)
|
||||
: startNode.relationshipFrom(endNode, relationshipType);
|
||||
|
||||
MapProjection mapProjection = projectAllPropertiesAndRelationships(endNodeDescription,
|
||||
relationshipFieldName, new ArrayList<>(processedRelationships));
|
||||
|
||||
if (relationshipDescription.hasRelationshipProperties()) {
|
||||
relationship = relationship.named(RelationshipDescription.NAME_OF_RELATIONSHIP);
|
||||
mapProjection = mapProjection.and(relationship);
|
||||
}
|
||||
|
||||
addMapProjection(relationshipTargetName,
|
||||
listBasedOn(relationship).returning(mapProjection),
|
||||
mapProjectionLists);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMapProjection(String name, Object projection, List<Object> projectionList) {
|
||||
projectionList.add(name);
|
||||
projectionList.add(projection);
|
||||
}
|
||||
|
||||
private static Condition conditionOrNoCondition(@Nullable Condition condition) {
|
||||
return condition == null ? Conditions.noCondition() : condition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* This annotation can be used on a field of type {@link java.util.Collection Collection<String>}. The content
|
||||
* of this field will be treated as dynamic or runtime managed labels. This means: All labels that are not statically
|
||||
* defined via the class hierarchy and the corresponding {@link Node @Node} annotation are added to this list while
|
||||
* loading the entity and all values contained in the collection will be added to the nodes labels.
|
||||
* <p>
|
||||
* Labels not defined through the class hierarchy or the list of dynamic labels will be removed from the database
|
||||
* when {@link DynamicLabels @DynamicLabels} is used.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Danger Dan - Nudeln und Klopapier
|
||||
* @since 1.1
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface DynamicLabels {
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Indicates a generated id. Ids can be generated internally. by the database itself or by an external generator. This annotation
|
||||
* defaults to the internally generated ids.
|
||||
* <p>
|
||||
* An internal id has no corresponding property on a node. It can only retrieved via the built-in Cypher function {@code id()}.
|
||||
* <p>
|
||||
* To use an external id generator, specify on the
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented
|
||||
@Inherited
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface GeneratedValue {
|
||||
|
||||
/**
|
||||
* @return The generator to use.
|
||||
* @see #generatorClass()
|
||||
*/
|
||||
@AliasFor("generatorClass")
|
||||
Class<? extends IdGenerator<?>> value() default GeneratedValue.InternalIdGenerator.class;
|
||||
|
||||
/**
|
||||
* @return The generator to use. Defaults to {@link InternalIdGenerator}, which indicates database generated values.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
Class<? extends IdGenerator<?>> generatorClass() default GeneratedValue.InternalIdGenerator.class;
|
||||
|
||||
/**
|
||||
* @return An optional reference to a bean to be used as ID generator.
|
||||
*/
|
||||
String generatorRef() default "";
|
||||
|
||||
/**
|
||||
* This {@link IdGenerator} does nothing. It is used for relying on the internal, database-side created id.
|
||||
*/
|
||||
final class InternalIdGenerator implements IdGenerator<Void> {
|
||||
|
||||
@Override
|
||||
public Void generateId(String primaryLabel, Object entity) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This generator is automatically applied when a field of type {@link java.util.UUID} is annotated with
|
||||
* {@link Id @Id} and {@link GeneratedValue @GeneratedValue}.
|
||||
*
|
||||
* @since 1.0.1
|
||||
*/
|
||||
final class UUIDGenerator implements IdGenerator<UUID> {
|
||||
|
||||
@Override
|
||||
public UUID generateId(String primaryLabel, Object entity) {
|
||||
return UUID.randomUUID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* Provides minimal information how to map class attributes to the properties of a node or a relationship.
|
||||
* <p>
|
||||
* Spring Data's persistent properties have slightly different semantics. They have an entity centric approach of properties.
|
||||
* Spring Data properties contain - if not marked otherwise - also associations.
|
||||
* <p>
|
||||
* Associations between different node types can be queried on the {@link Schema} itself.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface GraphPropertyDescription {
|
||||
|
||||
/**
|
||||
* @return The name of the attribute of the mapped class
|
||||
*/
|
||||
String getFieldName();
|
||||
|
||||
/**
|
||||
* @return The name of the property as stored in the graph.
|
||||
*/
|
||||
String getPropertyName();
|
||||
|
||||
/**
|
||||
* @return True if this property is the id property.
|
||||
*/
|
||||
boolean isIdProperty();
|
||||
|
||||
/**
|
||||
* @return True, if this property is the id property and the owner uses internal ids.
|
||||
*/
|
||||
boolean isInternalIdProperty();
|
||||
|
||||
/**
|
||||
* This will return the type of a simple property or the component type of a collection like property.
|
||||
*
|
||||
* @return The type of this property.
|
||||
*/
|
||||
Class<?> getActualType();
|
||||
|
||||
/**
|
||||
* @return Whether this property describes a relationship or not.
|
||||
*/
|
||||
boolean isRelationship();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* This annotation is included here for completeness. It marks an attribute as the primary id of a node entity. It can
|
||||
* be used as an alternative to {@link org.springframework.data.annotation.Id} and it may provide additional features
|
||||
* in the future.
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
* To use assigned ids, annotate an arbitrary attribute of your domain class with {@link org.springframework.data.annotation.Id}
|
||||
* or this annotation:
|
||||
* <pre>
|
||||
* @Node
|
||||
* public class MyEntity {
|
||||
* @Id
|
||||
* String theId;
|
||||
* }
|
||||
* </pre>
|
||||
* You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node property in which the assigned id is stored.
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
* To use internally generated ids, annotate an arbitrary attribute of type {@code java.lang.long} or {@code java.lang.Long}
|
||||
* with {@code @Id} and {@link GeneratedValue @GeneratedValue}.
|
||||
* <pre>
|
||||
* @Node
|
||||
* public class MyEntity {
|
||||
* @Id @GeneratedValue
|
||||
* Long id;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* It does not need to be named {@code id}, but most people chose this as the attribute in the class. As the attribute
|
||||
* does not correspond to a node property, it cannot be renamed via {@code @Property}.
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
* To use externally generated ids, annotate an arbitrary attribute with a type that your generated returns
|
||||
* with {@code @Id} and {@link GeneratedValue @GeneratedValue} and specify the generator class.
|
||||
*
|
||||
* <pre>
|
||||
* @Node
|
||||
* public class MyEntity {
|
||||
* @Id @GeneratedValue(UUIDStringGenerator.class)
|
||||
* String theId;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Externally generated ids are indistinguishable to assigned ids from the database perspective and thus can be arbitrarily
|
||||
* named via {@code @Property}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented
|
||||
@Inherited
|
||||
@org.springframework.data.annotation.Id
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface Id {
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import static org.neo4j.cypherdsl.core.Cypher.*;
|
||||
import static org.neo4j.springframework.data.core.schema.Constants.*;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Expression;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Description how to generate Ids for entities.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class IdDescription {
|
||||
|
||||
/**
|
||||
* The class representing a generator for new ids or null for assigned ids.
|
||||
*/
|
||||
private @Nullable final Class<? extends IdGenerator<?>> idGeneratorClass;
|
||||
|
||||
/**
|
||||
* A reference to an ID generator.
|
||||
*/
|
||||
private @Nullable final String idGeneratorRef;
|
||||
|
||||
/**
|
||||
* The property that stores the id if applicable.
|
||||
*/
|
||||
private @Nullable final String graphPropertyName;
|
||||
|
||||
private final Lazy<Expression> idExpression;
|
||||
|
||||
public static IdDescription forAssignedIds(String graphPropertyName) {
|
||||
|
||||
Assert.notNull(graphPropertyName, "Graph property name is required.");
|
||||
return new IdDescription(null, null, graphPropertyName);
|
||||
}
|
||||
|
||||
public static IdDescription forInternallyGeneratedIds() {
|
||||
return new IdDescription(GeneratedValue.InternalIdGenerator.class, null, null);
|
||||
}
|
||||
|
||||
public static IdDescription forExternallyGeneratedIds(
|
||||
@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
|
||||
@Nullable String idGeneratorRef,
|
||||
String graphPropertyName) {
|
||||
|
||||
Assert.notNull(graphPropertyName, "Graph property name is required.");
|
||||
try {
|
||||
Assert.hasText(idGeneratorRef, "Reference to an ID generator has precedence.");
|
||||
|
||||
return new IdDescription(null, idGeneratorRef, graphPropertyName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Assert.notNull(idGeneratorClass, "Class of id generator is required.");
|
||||
Assert.isTrue(idGeneratorClass != GeneratedValue.InternalIdGenerator.class,
|
||||
"Cannot use InternalIdGenerator for externally generated ids.");
|
||||
|
||||
return new IdDescription(idGeneratorClass, null, graphPropertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private IdDescription(
|
||||
@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
|
||||
@Nullable String idGeneratorRef,
|
||||
@Nullable String graphPropertyName
|
||||
) {
|
||||
this.idGeneratorClass = idGeneratorClass;
|
||||
this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef;
|
||||
this.graphPropertyName = graphPropertyName;
|
||||
this.idExpression = Lazy.of(() -> {
|
||||
final Node rootNode = anyNode(NAME_OF_ROOT_NODE);
|
||||
if (this.isInternallyGeneratedId()) {
|
||||
return Functions.id(rootNode);
|
||||
} else {
|
||||
return this.getOptionalGraphPropertyName()
|
||||
.map(propertyName -> property(NAME_OF_ROOT_NODE, propertyName)).get();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Expression asIdExpression() {
|
||||
return this.idExpression.get();
|
||||
}
|
||||
|
||||
public Optional<Class<? extends IdGenerator<?>>> getIdGeneratorClass() {
|
||||
return Optional.ofNullable(idGeneratorClass);
|
||||
}
|
||||
|
||||
public Optional<String> getIdGeneratorRef() {
|
||||
return Optional.ofNullable(idGeneratorRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True, if the ID is assigned to the entity before the entity hits the database, either manually or through a generator.
|
||||
*/
|
||||
public boolean isAssignedId() {
|
||||
return this.idGeneratorClass == null && this.idGeneratorRef == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True, if the database generated the ID.
|
||||
*/
|
||||
public boolean isInternallyGeneratedId() {
|
||||
return this.idGeneratorClass == GeneratedValue.InternalIdGenerator.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True, if the ID is externally generated.
|
||||
*/
|
||||
public boolean isExternallyGeneratedId() {
|
||||
return (this.idGeneratorClass != null && this.idGeneratorClass != GeneratedValue.InternalIdGenerator.class)
|
||||
|| this.idGeneratorRef != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ID description has only a corresponding graph property name when it's bas on an external assigment.
|
||||
* An internal id has no corresponding graph property and therefor this method
|
||||
* will return an empty {@link Optional} in such cases.
|
||||
*
|
||||
* @return The name of an optional graph property.
|
||||
*/
|
||||
public Optional<String> getOptionalGraphPropertyName() {
|
||||
return Optional.ofNullable(graphPropertyName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* Interface for generating ids for entities.
|
||||
*
|
||||
* @param <T> Type of the id to generate
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface IdGenerator<T> {
|
||||
|
||||
/**
|
||||
* Generates a new id for given entity.
|
||||
*
|
||||
* @param entity the entity to be saved
|
||||
* @return id to be assigned to the entity
|
||||
*/
|
||||
T generateId(String primaryLabel, Object entity);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* The annotation to configure the mapping from a node with a given set of labels to a class and vice versa.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@org.springframework.data.annotation.Persistent
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface Node {
|
||||
|
||||
/**
|
||||
* @return See {@link #labels()}.
|
||||
*/
|
||||
@AliasFor("labels")
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* @return The labels to identify a node with that is supposed to be mapped to the class annotated with {@link Node @Node}.
|
||||
* The first label will be the primary label if not {@link #primaryLabel()} was set explicitly.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String[] labels() default {};
|
||||
|
||||
/**
|
||||
* @return The explicit primary label to identify a node.
|
||||
*/
|
||||
String primaryLabel() default "";
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Expression;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and
|
||||
* access to the nodes properties.
|
||||
*
|
||||
* @param <T> The type of the underlying class
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface NodeDescription<T> {
|
||||
|
||||
/**
|
||||
* @return The primary label of this entity inside Neo4j.
|
||||
*/
|
||||
String getPrimaryLabel();
|
||||
|
||||
/**
|
||||
* @return the list of all additional labels (All labels except the {@link NodeDescription#getPrimaryLabel()}.
|
||||
*/
|
||||
List<String> getAdditionalLabels();
|
||||
|
||||
/**
|
||||
* @return The list of all static labels, that is the union of {@link #getPrimaryLabel()} + {@link #getAdditionalLabels()}.
|
||||
* Order is guaranteed to be the primary first, than the others.
|
||||
* @since 1.1
|
||||
*/
|
||||
default List<String> getStaticLabels() {
|
||||
List<String> staticLabels = new ArrayList<>();
|
||||
staticLabels.add(this.getPrimaryLabel());
|
||||
staticLabels.addAll(this.getAdditionalLabels());
|
||||
return staticLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The concrete class to which a node with the given {@link #getPrimaryLabel()} is mapped to
|
||||
*/
|
||||
Class<T> getUnderlyingClass();
|
||||
|
||||
/**
|
||||
* @return A description how to determine primary ids for nodes fitting this description
|
||||
*/
|
||||
@Nullable
|
||||
IdDescription getIdDescription();
|
||||
|
||||
/**
|
||||
* @return A collection of persistent properties that are mapped to graph properties and not to relationships
|
||||
*/
|
||||
Collection<GraphPropertyDescription> getGraphProperties();
|
||||
|
||||
/**
|
||||
* @return All graph properties including all properties from the extending classes if this entity is a parent entity.
|
||||
*/
|
||||
Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy();
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a {@link GraphPropertyDescription} by its field name.
|
||||
*
|
||||
* @param fieldName The field name for which the graph property description should be retrieved
|
||||
* @return An empty optional if there is no property known for the given field.
|
||||
*/
|
||||
Optional<GraphPropertyDescription> getGraphProperty(String fieldName);
|
||||
|
||||
/**
|
||||
* @return True if entities for this node use Neo4j internal ids.
|
||||
*/
|
||||
default boolean isUsingInternalIds() {
|
||||
return this.getIdDescription().isInternallyGeneratedId();
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the outgoing relationships this node has to other nodes.
|
||||
*
|
||||
* @return The relationships defined by instances of this node.
|
||||
*/
|
||||
Collection<RelationshipDescription> getRelationships();
|
||||
|
||||
/**
|
||||
* Register a direct child node description for this entity.
|
||||
*
|
||||
* @param child - {@link NodeDescription} that defines an extending class.
|
||||
*/
|
||||
void addChildNodeDescription(NodeDescription<?> child);
|
||||
|
||||
/**
|
||||
* Retrieve all direct child node descriptions which extend this entity.
|
||||
*
|
||||
* @return all direct child node description.
|
||||
*/
|
||||
Collection<NodeDescription<?>> getChildNodeDescriptionsInHierarchy();
|
||||
|
||||
/**
|
||||
* Register the direct parent node description.
|
||||
*
|
||||
* @param parent - {@link NodeDescription} that describes the parent entity.
|
||||
*/
|
||||
void setParentNodeDescription(NodeDescription<?> parent);
|
||||
|
||||
/**
|
||||
* @return An expression that represents the right identifier type.
|
||||
*/
|
||||
default Expression getIdExpression() {
|
||||
|
||||
return this.getIdDescription().asIdExpression();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* The annotation to configure the mapping from a property to an attribute and vice versa.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented
|
||||
@Inherited
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface Property {
|
||||
|
||||
/**
|
||||
* @return See {@link #name()}.
|
||||
*/
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The name of the property in the graph.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to configure mappings of relationship.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented
|
||||
@Inherited
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface Relationship {
|
||||
|
||||
/**
|
||||
* Enumeration of the direction a relationship can take.
|
||||
* @since 1.0
|
||||
*/
|
||||
enum Direction {
|
||||
|
||||
/**
|
||||
* Describes an outgoing relationship.
|
||||
*/
|
||||
OUTGOING,
|
||||
|
||||
/**
|
||||
* Describes an incoming relationship.
|
||||
*/
|
||||
INCOMING
|
||||
}
|
||||
|
||||
/**
|
||||
* @return See {@link #type()}.
|
||||
*/
|
||||
@AliasFor("type")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* @return The type of the relationship.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String type() default "";
|
||||
|
||||
/**
|
||||
* If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated with {@link Relationship} will be
|
||||
* the target node of the relationship and the class containing the annotated attribute will be the start node.
|
||||
* <p>
|
||||
* If {@code direction} is {@link Direction#INCOMING}, than the attribute annotated with {@link Relationship} will be
|
||||
* the start node of the relationship and the class containing the annotated attribute will be the end node.
|
||||
*
|
||||
* @return The direction of the relationship.
|
||||
*/
|
||||
Direction direction() default Direction.OUTGOING;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.schema.Relationship.Direction;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction
|
||||
* is maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by
|
||||
* {@link #getTarget}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface RelationshipDescription {
|
||||
|
||||
String NAME_OF_RELATIONSHIP = "__relationship__";
|
||||
|
||||
String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__";
|
||||
|
||||
/**
|
||||
* If this relationship is dynamic, than this method always returns the name of the inverse property.
|
||||
*
|
||||
* @return The type of this relationship
|
||||
*/
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* A relationship is dynamic when it's modelled as a {@code Map<String, ?>}.
|
||||
*
|
||||
* @return True, if this relationship is dynamic
|
||||
*/
|
||||
boolean isDynamic();
|
||||
|
||||
/**
|
||||
* The source of this relationship is described by the primary label of the node in question.
|
||||
*
|
||||
* @return The source of this relationship
|
||||
*/
|
||||
NodeDescription<?> getSource();
|
||||
|
||||
/**
|
||||
* The target of this relationship is described by the primary label of the node in question.
|
||||
*
|
||||
* @return The target of this relationship
|
||||
*/
|
||||
NodeDescription<?> getTarget();
|
||||
|
||||
/**
|
||||
* The name of the property where the relationship was defined. This is used by the Cypher creation to name the
|
||||
* return values.
|
||||
*
|
||||
* @return The name of the field storing the relationship property
|
||||
*/
|
||||
String getFieldName();
|
||||
|
||||
/**
|
||||
* The direction of the defined relationship. This is used by the Cypher creation to query for relationships
|
||||
* and create them with the right directions.
|
||||
*
|
||||
* @return The direction of the relationship
|
||||
*/
|
||||
Direction getDirection();
|
||||
|
||||
/**
|
||||
* If this is a relationship with properties, the properties-defining class will get returned,
|
||||
* otherwise {@literal null}.
|
||||
*
|
||||
* @return The type of the relationship property class for relationship with properties, otherwise {@literal null}
|
||||
*/
|
||||
@Nullable
|
||||
Class<?> getRelationshipPropertiesClass();
|
||||
|
||||
/**
|
||||
* Tells if this relationship is a relationship with additional properties.
|
||||
* In such cases {@code getRelationshipPropertiesClass} will return the type of the properties holding class.
|
||||
*
|
||||
* @return {@literal true} if an additional properties are available, otherwise {@literal false}
|
||||
*/
|
||||
boolean hasRelationshipProperties();
|
||||
|
||||
default boolean isOutgoing() {
|
||||
return Direction.OUTGOING.equals(this.getDirection());
|
||||
}
|
||||
|
||||
default boolean isIncoming() {
|
||||
return Direction.INCOMING.equals(this.getDirection());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
default String generateRelatedNodesCollectionName() {
|
||||
|
||||
return this.getSource().getPrimaryLabel() + "_" + this.getType() + "_" + this.getTarget().getPrimaryLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the relationship definition that describes the opposite side of the relationship.
|
||||
*
|
||||
* @param relationshipObverse logically same relationship definition in the target entity
|
||||
*/
|
||||
void setRelationshipObverse(RelationshipDescription relationshipObverse);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return logically same relationship definition in the target entity
|
||||
*/
|
||||
RelationshipDescription getRelationshipObverse();
|
||||
|
||||
/**
|
||||
* Checks if there is a relationship description describing the obverse of this relationship.
|
||||
*
|
||||
* @return true if a logically same relationship in the target entity exists, otherwise false.
|
||||
*/
|
||||
boolean hasRelationshipObverse();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* This marker interface is used on classes to mark that they represent additional relationship properties.
|
||||
* A class that implements this interface must not be used as a or annotated with {@link Node}.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Inherited
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public @interface RelationshipProperties {
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Contains the descriptions of all nodes, their properties and relationships known to SDN-RX.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface Schema {
|
||||
|
||||
/**
|
||||
* Registers the given set of classes to be available as Neo4j domain entities.
|
||||
*
|
||||
* @param initialEntitySet The set of classes to register with this schema
|
||||
*/
|
||||
void setInitialEntitySet(Set<? extends Class<?>> initialEntitySet);
|
||||
|
||||
/**
|
||||
* Triggers the scanning of the registered, initial entity set.
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Retrieves a nodes description by its primary label.
|
||||
*
|
||||
* @param primaryLabel The primary label under which the node is described
|
||||
* @return The description if any, null otherwise
|
||||
*/
|
||||
@Nullable NodeDescription<?> getNodeDescription(String primaryLabel);
|
||||
|
||||
/**
|
||||
* Retrieves a nodes description by its underlying class.
|
||||
*
|
||||
* @param underlyingClass The underlying class of the node description to be retrieved
|
||||
* @return The description if any, null otherwise
|
||||
*/
|
||||
@Nullable NodeDescription<?> getNodeDescription(Class<?> underlyingClass);
|
||||
|
||||
default NodeDescription<?> getRequiredNodeDescription(Class<?> underlyingClass) {
|
||||
NodeDescription<?> nodeDescription = getNodeDescription(underlyingClass);
|
||||
if (nodeDescription == null) {
|
||||
throw new UnknownEntityException(underlyingClass);
|
||||
}
|
||||
return nodeDescription;
|
||||
}
|
||||
|
||||
default NodeDescription<?> getRequiredNodeDescription(String primaryLabel) {
|
||||
NodeDescription<?> nodeDescription = getNodeDescription(primaryLabel);
|
||||
if (nodeDescription == null) {
|
||||
throw new MappingException(
|
||||
String.format("Required node description not found with primary label '%s'", primaryLabel));
|
||||
}
|
||||
return nodeDescription;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a
|
||||
* record containing all the nodes and relationships necessary to fully populate an instance of the given class.
|
||||
* It will not try to fetch data from any other records or queries. The mapping function is free to throw a {@link RuntimeException},
|
||||
* most likely a {@code org.springframework.data.mapping.MappingException} or {@link IllegalStateException} when
|
||||
* mapping is not possible.
|
||||
* <p>
|
||||
* In case the mapping function returns a {@literal null}, the Neo4j client will throw an exception and prevent further
|
||||
* processing.
|
||||
*
|
||||
* @param targetClass The target class to which to map to.
|
||||
* @param <T> Type of the target class
|
||||
* @return The default, stateless and reusable mapping function for the given target class
|
||||
* @throws UnknownEntityException When {@code targetClass} is not a managed class
|
||||
*/
|
||||
default <T> BiFunction<TypeSystem, Record, T> getRequiredMappingFunctionFor(Class<T> targetClass) {
|
||||
NodeDescription<?> nodeDescription = getNodeDescription(targetClass);
|
||||
if (nodeDescription == null) {
|
||||
throw new UnknownEntityException(targetClass);
|
||||
}
|
||||
return (typeSystem, record) -> getConverter().read(targetClass, record);
|
||||
}
|
||||
|
||||
Neo4jConverter getConverter();
|
||||
|
||||
default <T> Function<T, Map<String, Object>> getRequiredBinderFunctionFor(Class<T> sourceClass) {
|
||||
|
||||
if (getNodeDescription(sourceClass) == null) {
|
||||
throw new UnknownEntityException(sourceClass);
|
||||
}
|
||||
|
||||
return t -> {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
getConverter().write(t, parameters);
|
||||
return parameters;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or retrieves an instance of the given id generator class. During the lifetime of the schema,
|
||||
* this method returns the same instance of reoccurring requests of the same type.
|
||||
*
|
||||
* @param idGeneratorType The type of the ID generator to return
|
||||
* @return The id generator.
|
||||
*/
|
||||
<T extends IdGenerator<?>> T getOrCreateIdGeneratorOfType(Class<T> idGeneratorType);
|
||||
|
||||
<T extends IdGenerator<?>> Optional<T> getIdGenerator(String reference);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.schema;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
|
||||
/**
|
||||
* Thrown when required information about a class or primary label is requested from the {@link Schema} and those information
|
||||
* is not available.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class UnknownEntityException extends InvalidDataAccessApiUsageException {
|
||||
|
||||
private final Class<?> targetClass;
|
||||
|
||||
public UnknownEntityException(Class<?> targetClass) {
|
||||
super(String.format("%s is not a known entity", targetClass.getName()));
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
public Class<?> getTargetClass() {
|
||||
return targetClass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This package contains the schema that is defined by a set of classes, representing nodes and relationships and their
|
||||
* properties. It provides Neo4js main annotations to mark classes as persistable nodes.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core.schema;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.support;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Philipp Tölle
|
||||
*/
|
||||
public final class Relationships {
|
||||
|
||||
/**
|
||||
* The value for a relationship can be a scalar object (1:1), a collection (1:n), a map (1:n, but with dynamic
|
||||
* relationship types) or a map (1:n) with properties for each relationship.
|
||||
* This method unifies the type into something iterable, depending on the given inverse type.
|
||||
*
|
||||
* @param rawValue The raw value to unify
|
||||
* @return A unified collection (Either a collection of Map.Entry for dynamic and relationships with properties
|
||||
* or a list of related values)
|
||||
*/
|
||||
@Nullable
|
||||
public static Collection<?> unifyRelationshipValue(Neo4jPersistentProperty property, Object rawValue) {
|
||||
Collection<?> unifiedValue;
|
||||
if (property.isDynamicAssociation()) {
|
||||
if (property.isDynamicOneToManyAssociation()) {
|
||||
unifiedValue = ((Map<String, Collection<?>>) rawValue)
|
||||
.entrySet()
|
||||
.stream()
|
||||
.flatMap(e -> e.getValue().stream().map(v -> new SimpleEntry(e.getKey(), v)))
|
||||
.collect(toList());
|
||||
} else {
|
||||
unifiedValue = ((Map<String, Object>) rawValue).entrySet();
|
||||
}
|
||||
} else if (property.isRelationshipWithProperties()) {
|
||||
unifiedValue = ((Map<Object, Object>) rawValue).entrySet();
|
||||
} else if (property.isCollectionLike()) {
|
||||
unifiedValue = (Collection<Object>) rawValue;
|
||||
} else {
|
||||
unifiedValue = Collections.singleton(rawValue);
|
||||
}
|
||||
return unifiedValue;
|
||||
}
|
||||
|
||||
private Relationships() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.support;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.springframework.data.core.schema.IdGenerator;
|
||||
|
||||
/**
|
||||
* A generator providing UUIDs.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack)
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class UUIDStringGenerator implements IdGenerator<String> {
|
||||
|
||||
@Override
|
||||
public String generateId(String primaryLabel, Object entity) {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.neo4j.driver.Bookmark;
|
||||
|
||||
/**
|
||||
* Responsible for storing, updating and retrieving the bookmarks of Neo4j's transaction.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Metallica - Death Magnetic
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Neo4jBookmarkManager {
|
||||
|
||||
private Set<Bookmark> bookmarks = new HashSet<>();
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final Lock read = lock.readLock();
|
||||
private final Lock write = lock.writeLock();
|
||||
|
||||
Collection<Bookmark> getBookmarks() {
|
||||
|
||||
try {
|
||||
read.lock();
|
||||
return Collections.unmodifiableSet(new HashSet<>(bookmarks));
|
||||
} finally {
|
||||
read.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void updateBookmarks(Collection<Bookmark> usedBookmarks, Bookmark lastBookmark) {
|
||||
|
||||
try {
|
||||
write.lock();
|
||||
bookmarks.removeAll(usedBookmarks);
|
||||
bookmarks.add(lastBookmark);
|
||||
} finally {
|
||||
write.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.transaction.support.ResourceHolderSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
|
||||
/**
|
||||
* Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when
|
||||
* participating in a non-native Neo4j transaction, such as a Jta transaction.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Neo4jSessionSynchronization
|
||||
extends ResourceHolderSynchronization<Neo4jTransactionHolder, Object> {
|
||||
|
||||
private final Neo4jTransactionHolder localConnectionHolder;
|
||||
|
||||
Neo4jSessionSynchronization(Neo4jTransactionHolder connectionHolder, Driver driver) {
|
||||
|
||||
super(connectionHolder, driver);
|
||||
this.localConnectionHolder = connectionHolder;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion()
|
||||
*/
|
||||
@Override
|
||||
protected boolean shouldReleaseBeforeCompletion() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
protected void processResourceAfterCommit(Neo4jTransactionHolder resourceHolder) {
|
||||
|
||||
super.processResourceAfterCommit(resourceHolder);
|
||||
|
||||
if (resourceHolder.hasActiveTransaction()) {
|
||||
resourceHolder.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int)
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
|
||||
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localConnectionHolder.hasActiveTransaction()) {
|
||||
localConnectionHolder.rollback();
|
||||
}
|
||||
|
||||
super.afterCompletion(status);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
protected void releaseResource(Neo4jTransactionHolder resourceHolder, Object resourceKey) {
|
||||
|
||||
if (resourceHolder.hasActiveSession()) {
|
||||
resourceHolder.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.neo4j.driver.Bookmark;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the context in which a transaction has been opened. The context consists primarly of the target database
|
||||
* and the set of bookmarks used to start the session from.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Evanescence - Fallen
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Neo4jTransactionContext {
|
||||
|
||||
/**
|
||||
* The target database of the session.
|
||||
*/
|
||||
private @Nullable final String databaseName;
|
||||
|
||||
/**
|
||||
* The bookmarks from which that session was started. Maybe empty but never null.
|
||||
*/
|
||||
private final Collection<Bookmark> bookmarks;
|
||||
|
||||
Neo4jTransactionContext(@Nullable String databaseName) {
|
||||
|
||||
this(databaseName, Collections.emptyList());
|
||||
}
|
||||
|
||||
Neo4jTransactionContext(@Nullable String databaseName, Collection<Bookmark> bookmarks) {
|
||||
this.databaseName = databaseName;
|
||||
this.bookmarks = bookmarks;
|
||||
}
|
||||
|
||||
String getDatabaseName() {
|
||||
return databaseName;
|
||||
}
|
||||
|
||||
Collection<Bookmark> getBookmarks() {
|
||||
return bookmarks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.neo4j.driver.Bookmark;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.ResourceHolderSupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.Transaction}.
|
||||
* {@link Neo4jTransactionManager} binds instances of this class to the thread.
|
||||
* <p>
|
||||
* <strong>Note:</strong> Intended for internal usage only.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Neo4jTransactionHolder extends ResourceHolderSupport {
|
||||
|
||||
private final Neo4jTransactionContext context;
|
||||
/**
|
||||
* The ongoing session...
|
||||
*/
|
||||
private final Session session;
|
||||
/**
|
||||
* The drivers transaction as the second building block of what synchronize our transaction against.
|
||||
*/
|
||||
private final Transaction transaction;
|
||||
|
||||
Neo4jTransactionHolder(Neo4jTransactionContext context, Session session, Transaction transaction) {
|
||||
|
||||
this.context = context;
|
||||
this.session = session;
|
||||
this.transaction = transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction if it has been opened in a session for the requested database or an empty optional.
|
||||
*
|
||||
* @param inDatabase selected database to use
|
||||
* @return An optional, ongoing transaction.
|
||||
*/
|
||||
@Nullable Transaction getTransaction(String inDatabase) {
|
||||
return namesMapToTheSameDatabase(this.context.getDatabaseName(), inDatabase) ? transaction : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Bookmark commit() {
|
||||
|
||||
Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed.");
|
||||
Assert.state(!isRollbackOnly(), "Resource must not be marked as rollback only.");
|
||||
|
||||
transaction.commit();
|
||||
transaction.close();
|
||||
|
||||
return session.lastBookmark();
|
||||
}
|
||||
|
||||
void rollback() {
|
||||
|
||||
Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed.");
|
||||
|
||||
transaction.rollback();
|
||||
transaction.close();
|
||||
}
|
||||
|
||||
void close() {
|
||||
|
||||
Assert.state(hasActiveSession(), "Session must be open, but has already been closed.");
|
||||
|
||||
if (hasActiveTransaction()) {
|
||||
transaction.close();
|
||||
}
|
||||
session.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
|
||||
super.setRollbackOnly();
|
||||
transaction.rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetRollbackOnly() {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
boolean hasActiveSession() {
|
||||
|
||||
return session.isOpen();
|
||||
}
|
||||
|
||||
boolean hasActiveTransaction() {
|
||||
|
||||
return transaction.isOpen();
|
||||
}
|
||||
|
||||
String getDatabaseName() {
|
||||
return context.getDatabaseName();
|
||||
}
|
||||
|
||||
Collection<Bookmark> getBookmarks() {
|
||||
return context.getBookmarks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Bookmark;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.neo4j.driver.TransactionConfig;
|
||||
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionException;
|
||||
import org.springframework.transaction.TransactionSystemException;
|
||||
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
|
||||
import org.springframework.transaction.support.DefaultTransactionStatus;
|
||||
import org.springframework.transaction.support.SmartTransactionObject;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This
|
||||
* transaction manager will synchronize a pair of a native Neo4j session/transaction with the transaction.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager {
|
||||
|
||||
/**
|
||||
* The underlying driver, which is also the synchronisation object.
|
||||
*/
|
||||
private final Driver driver;
|
||||
|
||||
/**
|
||||
* Database name provider.
|
||||
*/
|
||||
private final DatabaseSelectionProvider databaseSelectionProvider;
|
||||
|
||||
private final Neo4jBookmarkManager bookmarkManager;
|
||||
|
||||
public Neo4jTransactionManager(Driver driver) {
|
||||
|
||||
this(driver, DatabaseSelectionProvider.getDefaultSelectionProvider());
|
||||
}
|
||||
|
||||
public Neo4jTransactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) {
|
||||
|
||||
this.driver = driver;
|
||||
this.databaseSelectionProvider = databaseSelectionProvider;
|
||||
this.bookmarkManager = new Neo4jBookmarkManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods provides a native Neo4j transaction to be used from within a {@link org.neo4j.springframework.data.core.Neo4jClient}.
|
||||
* In most cases this the native transaction will be controlled from the Neo4j specific
|
||||
* {@link org.springframework.transaction.PlatformTransactionManager}. However, SDN-RX provides support for other
|
||||
* transaction managers as well. This methods registers a session synchronization in such cases on the foreign transaction manager.
|
||||
*
|
||||
* @param driver The driver that has been used as a synchronization object.
|
||||
* @param targetDatabase The target database
|
||||
* @return An optional managed transaction or {@literal null} if the method hasn't been called inside
|
||||
* an ongoing Spring transaction
|
||||
*/
|
||||
public static @Nullable Transaction retrieveTransaction(final Driver driver,
|
||||
@Nullable final String targetDatabase) {
|
||||
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check whether we have a transaction managed by a Neo4j transaction manager
|
||||
Neo4jTransactionHolder connectionHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager
|
||||
.getResource(driver);
|
||||
|
||||
if (connectionHolder != null) {
|
||||
Transaction optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase);
|
||||
|
||||
if (optionalOngoingTransaction != null) {
|
||||
return optionalOngoingTransaction;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
|
||||
}
|
||||
|
||||
// Otherwise we open a session and synchronize it.
|
||||
Session session = driver.session(defaultSessionConfig(targetDatabase));
|
||||
Transaction transaction = session.beginTransaction(TransactionConfig.empty());
|
||||
// Manually create a new synchronization
|
||||
connectionHolder = new Neo4jTransactionHolder(new Neo4jTransactionContext(targetDatabase), session, transaction);
|
||||
connectionHolder.setSynchronizedWithTransaction(true);
|
||||
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new Neo4jSessionSynchronization(connectionHolder, driver));
|
||||
|
||||
TransactionSynchronizationManager.bindResource(driver, connectionHolder);
|
||||
return connectionHolder.getTransaction(targetDatabase);
|
||||
}
|
||||
|
||||
private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) {
|
||||
|
||||
Assert.isInstanceOf(Neo4jTransactionObject.class, transaction,
|
||||
() -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class,
|
||||
transaction.getClass()));
|
||||
|
||||
return (Neo4jTransactionObject) transaction;
|
||||
}
|
||||
|
||||
private static Neo4jTransactionObject extractNeo4jTransaction(DefaultTransactionStatus status) {
|
||||
|
||||
return extractNeo4jTransaction(status.getTransaction());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doGetTransaction() throws TransactionException {
|
||||
|
||||
Neo4jTransactionHolder resourceHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager
|
||||
.getResource(driver);
|
||||
return new Neo4jTransactionObject(resourceHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
|
||||
|
||||
return extractNeo4jTransaction(transaction).hasResourceHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
|
||||
TransactionConfig transactionConfig = createTransactionConfigFrom(definition);
|
||||
boolean readOnly = definition.isReadOnly();
|
||||
|
||||
|
||||
TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly);
|
||||
|
||||
try {
|
||||
// Prepare configuration data
|
||||
Neo4jTransactionContext context = new Neo4jTransactionContext(
|
||||
databaseSelectionProvider.getDatabaseSelection().getValue(),
|
||||
bookmarkManager.getBookmarks()
|
||||
);
|
||||
|
||||
// Configure and open session together with a native transaction
|
||||
Session session = this.driver
|
||||
.session(sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseName()));
|
||||
Transaction nativeTransaction = session.beginTransaction(transactionConfig);
|
||||
|
||||
// Synchronize on that
|
||||
Neo4jTransactionHolder transactionHolder = new Neo4jTransactionHolder(context, session, nativeTransaction);
|
||||
transactionHolder.setSynchronizedWithTransaction(true);
|
||||
transactionObject.setResourceHolder(transactionHolder);
|
||||
|
||||
TransactionSynchronizationManager.bindResource(this.driver, transactionHolder);
|
||||
} catch (Exception ex) {
|
||||
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doSuspend(Object transaction) throws TransactionException {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
transactionObject.setResourceHolder(null);
|
||||
|
||||
return TransactionSynchronizationManager.unbindResource(driver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
transactionObject.setResourceHolder((Neo4jTransactionHolder) suspendedResources);
|
||||
|
||||
TransactionSynchronizationManager.bindResource(driver, suspendedResources);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
|
||||
Neo4jTransactionHolder transactionHolder = transactionObject.getRequiredResourceHolder();
|
||||
Bookmark lastBookmark = transactionHolder.commit();
|
||||
this.bookmarkManager.updateBookmarks(transactionHolder.getBookmarks(), lastBookmark);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
|
||||
transactionObject.getRequiredResourceHolder().rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
|
||||
transactionObject.setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCleanupAfterCompletion(Object transaction) {
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
transactionObject.getRequiredResourceHolder().close();
|
||||
transactionObject.setResourceHolder(null);
|
||||
TransactionSynchronizationManager.unbindResource(driver);
|
||||
}
|
||||
|
||||
|
||||
static class Neo4jTransactionObject implements SmartTransactionObject {
|
||||
|
||||
private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O";
|
||||
|
||||
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
|
||||
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
|
||||
// If it is null, there's no existing session / transaction.
|
||||
@Nullable
|
||||
private Neo4jTransactionHolder resourceHolder;
|
||||
|
||||
Neo4jTransactionObject(@Nullable Neo4jTransactionHolder resourceHolder) {
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's
|
||||
* no existing transaction.
|
||||
*
|
||||
* @param resourceHolder A newly created resource holder with a fresh drivers session,
|
||||
*/
|
||||
void setResourceHolder(@Nullable Neo4jTransactionHolder resourceHolder) {
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a {@link Neo4jTransactionHolder} is set.
|
||||
*/
|
||||
boolean hasResourceHolder() {
|
||||
return resourceHolder != null;
|
||||
}
|
||||
|
||||
Neo4jTransactionHolder getRequiredResourceHolder() {
|
||||
|
||||
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
|
||||
return resourceHolder;
|
||||
}
|
||||
|
||||
void setRollbackOnly() {
|
||||
|
||||
getRequiredResourceHolder().setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRollbackOnly() {
|
||||
return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
|
||||
TransactionSynchronizationUtils.triggerFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.neo4j.driver.AccessMode;
|
||||
import org.neo4j.driver.TransactionConfig;
|
||||
import org.neo4j.driver.SessionConfig;
|
||||
import org.neo4j.driver.Bookmark;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.IllegalTransactionStateException;
|
||||
import org.springframework.transaction.InvalidIsolationLevelException;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
|
||||
/**
|
||||
* Internal use only.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class Neo4jTransactionUtils {
|
||||
|
||||
/**
|
||||
* The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks.
|
||||
*
|
||||
* @param databaseName The database to use. May be null, which then designates the default database.
|
||||
* @return Session parameters to configure the default session used
|
||||
*/
|
||||
public static SessionConfig defaultSessionConfig(@Nullable String databaseName) {
|
||||
return sessionConfig(false, Collections.emptyList(), databaseName);
|
||||
}
|
||||
|
||||
public static SessionConfig sessionConfig(boolean readOnly, Collection<Bookmark> bookmarks,
|
||||
@Nullable String databaseName) {
|
||||
SessionConfig.Builder builder = SessionConfig.builder()
|
||||
.withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE)
|
||||
.withBookmarks(bookmarks);
|
||||
|
||||
if (databaseName != null) {
|
||||
builder.withDatabase(databaseName);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a Spring {@link TransactionDefinition transaction definition} to a native Neo4j driver transaction.
|
||||
* Only the default isolation leven ({@link TransactionDefinition#ISOLATION_DEFAULT}) and
|
||||
* {@link TransactionDefinition#PROPAGATION_REQUIRED propagation required} behaviour are supported.
|
||||
*
|
||||
* @param definition The transaction definition passed to a Neo4j transaction manager
|
||||
* @return A Neo4j native transaction configuration
|
||||
*/
|
||||
static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) {
|
||||
|
||||
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
|
||||
throw new InvalidIsolationLevelException(
|
||||
"Neo4jTransactionManager is not allowed to support custom isolation levels.");
|
||||
}
|
||||
|
||||
int propagationBehavior = definition.getPropagationBehavior();
|
||||
if (!(propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRED || propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRES_NEW)) {
|
||||
throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' or 'requires new' propagation.");
|
||||
}
|
||||
|
||||
TransactionConfig.Builder builder = TransactionConfig.builder();
|
||||
if (definition.getTimeout() > 0) {
|
||||
builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static boolean namesMapToTheSameDatabase(@Nullable String name1, @Nullable String name2) {
|
||||
return Objects.equals(name1, name2);
|
||||
}
|
||||
|
||||
static String formatOngoingTxInAnotherDbErrorMessage(String currentDb, String requestedDb) {
|
||||
String defaultDatabase = "the default database";
|
||||
String _currentDb = currentDb == null ? defaultDatabase : String.format("'%s'", currentDb);
|
||||
String _requestedDb = requestedDb == null ? defaultDatabase : String.format("'%s'", requestedDb);
|
||||
|
||||
return String.format("There is already an ongoing Spring transaction for %s, but you request %s", _currentDb,
|
||||
_requestedDb);
|
||||
|
||||
}
|
||||
|
||||
private Neo4jTransactionUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.transaction.reactive.ReactiveResourceSynchronization;
|
||||
import org.springframework.transaction.reactive.TransactionSynchronization;
|
||||
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class ReactiveNeo4jSessionSynchronization extends ReactiveResourceSynchronization<ReactiveNeo4jTransactionHolder, Object> {
|
||||
|
||||
private final ReactiveNeo4jTransactionHolder transactionHolder;
|
||||
|
||||
ReactiveNeo4jSessionSynchronization(TransactionSynchronizationManager transactionSynchronizationManager,
|
||||
ReactiveNeo4jTransactionHolder transactionHolder, Driver driver) {
|
||||
|
||||
super(transactionHolder, driver, transactionSynchronizationManager);
|
||||
|
||||
this.transactionHolder = transactionHolder;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#shouldReleaseBeforeCompletion()
|
||||
*/
|
||||
@Override
|
||||
protected boolean shouldReleaseBeforeCompletion() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#processResourceAfterCommit(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
protected Mono<Void> processResourceAfterCommit(ReactiveNeo4jTransactionHolder resourceHolder) {
|
||||
return Mono.defer(() -> super.processResourceAfterCommit(resourceHolder).then(resourceHolder.commit())).then();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#afterCompletion(int)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> afterCompletion(int status) {
|
||||
return Mono.defer(() -> {
|
||||
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
|
||||
return transactionHolder.rollback().then(super.afterCompletion(status));
|
||||
}
|
||||
return super.afterCompletion(status);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#releaseResource(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
protected Mono<Void> releaseResource(ReactiveNeo4jTransactionHolder resourceHolder, Object resourceKey) {
|
||||
return Mono.defer(() -> Mono.from(resourceHolder.getSession().close()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.neo4j.driver.Bookmark;
|
||||
import org.neo4j.driver.reactive.RxSession;
|
||||
import org.neo4j.driver.reactive.RxTransaction;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.ResourceHolderSupport;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport {
|
||||
|
||||
private final Neo4jTransactionContext context;
|
||||
private final RxSession session;
|
||||
private final RxTransaction transaction;
|
||||
|
||||
ReactiveNeo4jTransactionHolder(Neo4jTransactionContext context, RxSession session, RxTransaction transaction) {
|
||||
|
||||
this.context = context;
|
||||
this.session = session;
|
||||
this.transaction = transaction;
|
||||
}
|
||||
|
||||
RxSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Nullable RxTransaction getTransaction(String inDatabase) {
|
||||
|
||||
return namesMapToTheSameDatabase(this.context.getDatabaseName(), inDatabase) ? transaction : null;
|
||||
}
|
||||
|
||||
Mono<Bookmark> commit() {
|
||||
|
||||
return Mono.from(transaction.commit()).then(Mono.fromSupplier(() -> session.lastBookmark()));
|
||||
}
|
||||
|
||||
Mono<Void> rollback() {
|
||||
|
||||
return Mono.from(transaction.rollback());
|
||||
}
|
||||
|
||||
Mono<Void> close() {
|
||||
|
||||
return Mono.from(session.close());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
|
||||
super.setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetRollbackOnly() {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
String getDatabaseName() {
|
||||
return context.getDatabaseName();
|
||||
}
|
||||
|
||||
Collection<Bookmark> getBookmarks() {
|
||||
return context.getBookmarks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.TransactionConfig;
|
||||
import org.neo4j.driver.reactive.RxSession;
|
||||
import org.neo4j.driver.reactive.RxTransaction;
|
||||
import org.neo4j.springframework.data.core.DatabaseSelection;
|
||||
import org.neo4j.springframework.data.core.ReactiveDatabaseSelectionProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.NoTransactionException;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionException;
|
||||
import org.springframework.transaction.reactive.AbstractReactiveTransactionManager;
|
||||
import org.springframework.transaction.reactive.GenericReactiveTransaction;
|
||||
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.SmartTransactionObject;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager {
|
||||
|
||||
/**
|
||||
* The underlying driver, which is also the synchronisation object.
|
||||
*/
|
||||
private final Driver driver;
|
||||
|
||||
/**
|
||||
* Database name provider.
|
||||
*/
|
||||
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
|
||||
|
||||
private final Neo4jBookmarkManager bookmarkManager;
|
||||
|
||||
public ReactiveNeo4jTransactionManager(Driver driver) {
|
||||
this(driver, ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider());
|
||||
}
|
||||
|
||||
public ReactiveNeo4jTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) {
|
||||
|
||||
this.driver = driver;
|
||||
this.databaseSelectionProvider = databaseSelectionProvider;
|
||||
this.bookmarkManager = new Neo4jBookmarkManager();
|
||||
}
|
||||
|
||||
public static Mono<RxTransaction> retrieveReactiveTransaction(final Driver driver, final String targetDatabase) {
|
||||
|
||||
return TransactionSynchronizationManager.forCurrentTransaction() // Do we have a Transaction context?
|
||||
// Bail out early if synchronization between transaction managers is not active
|
||||
.filter(TransactionSynchronizationManager::isSynchronizationActive)
|
||||
.flatMap(tsm -> {
|
||||
// Get an existing holder
|
||||
ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm
|
||||
.getResource(driver);
|
||||
|
||||
// And use it if there is any
|
||||
if (existingTxHolder != null) {
|
||||
return Mono.just(existingTxHolder);
|
||||
}
|
||||
|
||||
// Otherwise open up a new native transaction
|
||||
return Mono.defer(() -> {
|
||||
RxSession session = driver.rxSession(defaultSessionConfig(targetDatabase));
|
||||
return Mono.from(session.beginTransaction(TransactionConfig.empty())).map(tx -> {
|
||||
|
||||
ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder(
|
||||
new Neo4jTransactionContext(targetDatabase), session, tx);
|
||||
newConnectionHolder.setSynchronizedWithTransaction(true);
|
||||
|
||||
tsm.registerSynchronization(
|
||||
new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver));
|
||||
|
||||
tsm.bindResource(driver, newConnectionHolder);
|
||||
return newConnectionHolder;
|
||||
});
|
||||
});
|
||||
})
|
||||
.map(connectionHolder -> {
|
||||
RxTransaction transaction = connectionHolder.getTransaction(targetDatabase);
|
||||
if (transaction == null) {
|
||||
throw new IllegalStateException(
|
||||
formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
)
|
||||
// If not, than just don't open a transaction
|
||||
.onErrorResume(NoTransactionException.class, nte -> Mono.empty());
|
||||
}
|
||||
|
||||
private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(Object transaction) {
|
||||
|
||||
Assert.isInstanceOf(ReactiveNeo4jTransactionObject.class, transaction,
|
||||
() -> String.format("Expected to find a %s but it turned out to be %s.", ReactiveNeo4jTransactionObject.class,
|
||||
transaction.getClass()));
|
||||
|
||||
return (ReactiveNeo4jTransactionObject) transaction;
|
||||
}
|
||||
|
||||
private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(GenericReactiveTransaction status) {
|
||||
return extractNeo4jTransaction(status.getTransaction());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doGetTransaction(TransactionSynchronizationManager transactionSynchronizationManager)
|
||||
throws TransactionException {
|
||||
|
||||
ReactiveNeo4jTransactionHolder resourceHolder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager
|
||||
.getResource(driver);
|
||||
return new ReactiveNeo4jTransactionObject(resourceHolder);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(Object)
|
||||
*/
|
||||
@Override
|
||||
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
|
||||
|
||||
return extractNeo4jTransaction(transaction).hasResourceHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> doBegin(TransactionSynchronizationManager transactionSynchronizationManager, Object transaction,
|
||||
TransactionDefinition transactionDefinition) throws TransactionException {
|
||||
|
||||
return Mono.defer(() -> {
|
||||
ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
|
||||
TransactionConfig transactionConfig = createTransactionConfigFrom(transactionDefinition);
|
||||
boolean readOnly = transactionDefinition.isReadOnly();
|
||||
|
||||
transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly);
|
||||
|
||||
return databaseSelectionProvider.getDatabaseSelection()
|
||||
.switchIfEmpty(Mono.just(DatabaseSelection.undecided()))
|
||||
.map(databaseName -> new Neo4jTransactionContext(databaseName.getValue(), bookmarkManager.getBookmarks()))
|
||||
.map(context -> Tuples.of(context, this.driver.rxSession(sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseName()))))
|
||||
.flatMap(contextAndSession -> Mono
|
||||
.from(contextAndSession.getT2().beginTransaction(transactionConfig))
|
||||
.map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(), contextAndSession.getT2(), nativeTransaction))
|
||||
)
|
||||
.doOnNext(transactionHolder -> {
|
||||
transactionHolder.setSynchronizedWithTransaction(true);
|
||||
transactionObject.setResourceHolder(transactionHolder);
|
||||
transactionSynchronizationManager.bindResource(this.driver, transactionHolder);
|
||||
});
|
||||
|
||||
}).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager,
|
||||
Object transaction) {
|
||||
|
||||
return Mono
|
||||
.just(extractNeo4jTransaction(transaction))
|
||||
.map(r -> {
|
||||
ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder();
|
||||
r.setResourceHolder(null);
|
||||
return holder;
|
||||
})
|
||||
.flatMap(ReactiveNeo4jTransactionHolder::close)
|
||||
.then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(driver)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> doCommit(TransactionSynchronizationManager transactionSynchronizationManager,
|
||||
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
|
||||
|
||||
ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction)
|
||||
.getRequiredResourceHolder();
|
||||
return holder.commit()
|
||||
.doOnNext(bookmark -> bookmarkManager.updateBookmarks(holder.getBookmarks(), bookmark))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> doRollback(TransactionSynchronizationManager transactionSynchronizationManager,
|
||||
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
|
||||
|
||||
ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction)
|
||||
.getRequiredResourceHolder();
|
||||
return holder.rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Object> doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) throws TransactionException {
|
||||
|
||||
return Mono
|
||||
.just(extractNeo4jTransaction(transaction))
|
||||
.doOnNext(r -> r.setResourceHolder(null))
|
||||
.then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(driver)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, Object transaction, Object suspendedResources) throws TransactionException {
|
||||
|
||||
return Mono
|
||||
.just(extractNeo4jTransaction(transaction))
|
||||
.doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources))
|
||||
.then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources)));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction)
|
||||
*/
|
||||
@Override
|
||||
protected Mono<Void> doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager,
|
||||
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(genericReactiveTransaction);
|
||||
transactionObject.getRequiredResourceHolder().setRollbackOnly();
|
||||
});
|
||||
}
|
||||
|
||||
static class ReactiveNeo4jTransactionObject implements SmartTransactionObject {
|
||||
|
||||
private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O";
|
||||
|
||||
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
|
||||
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
|
||||
// If it is null, there's no existing session / transaction.
|
||||
@Nullable private ReactiveNeo4jTransactionHolder resourceHolder;
|
||||
|
||||
ReactiveNeo4jTransactionObject(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) {
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usually called in {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} which is
|
||||
* called when there's no existing transaction.
|
||||
*
|
||||
* @param resourceHolder A newly created resource holder with a fresh drivers session,
|
||||
*/
|
||||
void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) {
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a {@link Neo4jTransactionHolder} is set.
|
||||
*/
|
||||
boolean hasResourceHolder() {
|
||||
return resourceHolder != null;
|
||||
}
|
||||
|
||||
ReactiveNeo4jTransactionHolder getRequiredResourceHolder() {
|
||||
|
||||
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
|
||||
return resourceHolder;
|
||||
}
|
||||
|
||||
void setRollbackOnly() {
|
||||
|
||||
getRequiredResourceHolder().setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRollbackOnly() {
|
||||
return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
|
||||
TransactionSynchronizationUtils.triggerFlush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Core infrastructure for providing Neo4j sessions to Spring Data Neo4j.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core.transaction;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
|
||||
/**
|
||||
* Neo4j specific {@link org.springframework.data.repository.Repository} interface.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Ján Šúr
|
||||
* @param <T> type of the domain class to map
|
||||
* @param <ID> identifier type in the domain class
|
||||
* @since 1.0
|
||||
*/
|
||||
@NoRepositoryBean
|
||||
public interface Neo4jRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.CrudRepository#saveAll(java.lang.Iterable)
|
||||
*/
|
||||
@Override <S extends T> List<S> saveAll(Iterable<S> entities);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.CrudRepository#findAll()
|
||||
*/
|
||||
@Override
|
||||
List<T> findAll();
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.CrudRepository#findAllById(java.lang.Iterable)
|
||||
*/
|
||||
@Override
|
||||
List<T> findAllById(Iterable<ID> iterable);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
|
||||
*/
|
||||
@Override
|
||||
List<T> findAll(Sort sort);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
|
||||
*/
|
||||
@Override <S extends T> List<S> findAll(Example<S> example);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
|
||||
*/
|
||||
@Override <S extends T> List<S> findAll(Example<S> example, Sort sort);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.repository;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
/**
|
||||
* Throw when a query doesn't return a required result.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Deichkind - Niveau weshalb warum
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public class NoResultException extends EmptyResultDataAccessException {
|
||||
|
||||
private final String query;
|
||||
|
||||
public NoResultException(int expectedNumberOfResults, String query) {
|
||||
super(expectedNumberOfResults);
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.repository;
|
||||
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
|
||||
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
|
||||
|
||||
/**
|
||||
* Neo4j specific {@link org.springframework.data.repository.Repository} interface with reactive support.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @param <T> type of the domain class to map
|
||||
* @param <ID> identifier type in the domain class
|
||||
* @since 1.0
|
||||
*/
|
||||
@NoRepositoryBean
|
||||
public interface ReactiveNeo4jRepository<T, ID>
|
||||
extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.repository.config;
|
||||
|
||||
import static org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension.*;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
|
||||
|
||||
/**
|
||||
* Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()},
|
||||
* {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated
|
||||
* configuration class.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Import(Neo4jRepositoriesRegistrar.class)
|
||||
public @interface EnableNeo4jRepositories {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
|
||||
* {@code @EnableNeo4jRepositories("org.my.pkg")} instead of
|
||||
* {@code @EnableNeo4jRepositories(basePackages="org.my.pkg")}.
|
||||
*/
|
||||
@AliasFor("basePackages")
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
|
||||
* attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
|
||||
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
|
||||
* each package that serves no purpose other than being referenced by this attribute.
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
|
||||
/**
|
||||
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
|
||||
* {@link Neo4jRepositoryFactoryBean}.
|
||||
*/
|
||||
Class<?> repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class;
|
||||
|
||||
/**
|
||||
* Configure the repository base class to be used to create repository proxies for this particular configuration.
|
||||
*
|
||||
* @return The base class to be used when creating repository proxies.
|
||||
*/
|
||||
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected.
|
||||
*/
|
||||
String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.Neo4jTemplate} bean to be used with the repositories detected.
|
||||
*/
|
||||
String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager} bean to be used with the repositories detected.
|
||||
*/
|
||||
String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
|
||||
* everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
|
||||
*/
|
||||
ComponentScan.Filter[] includeFilters() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are not eligible for component scanning.
|
||||
*/
|
||||
ComponentScan.Filter[] excludeFilters() default {};
|
||||
|
||||
/**
|
||||
* Configures the location of where to find the Spring Data named queries properties file. Will default to
|
||||
* {@code META-INFO/neo4j-named-queries.properties}.
|
||||
*/
|
||||
String namedQueriesLocation() default "";
|
||||
|
||||
/**
|
||||
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
|
||||
* for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
|
||||
* for {@code PersonRepositoryImpl}.
|
||||
*/
|
||||
String repositoryImplementationPostfix() default "Impl";
|
||||
|
||||
/**
|
||||
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
|
||||
* repositories infrastructure.
|
||||
*/
|
||||
boolean considerNestedRepositories() default false;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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.neo4j.springframework.data.repository.config;
|
||||
|
||||
import static org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension.*;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.neo4j.springframework.data.repository.support.ReactiveNeo4jRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
|
||||
|
||||
/**
|
||||
* Annotation to activate reactive Neo4j repositories. If no base package is configured through either {@link #value()},
|
||||
* {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated
|
||||
* configuration class.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Import(ReactiveNeo4jRepositoriesRegistrar.class)
|
||||
public @interface EnableReactiveNeo4jRepositories {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
|
||||
* {@code @EnableReactiveNeo4jRepositories("org.my.pkg")} instead of
|
||||
* {@code @EnableReactiveNeo4jRepositories(basePackages="org.my.pkg")}.
|
||||
*/
|
||||
@AliasFor("basePackages")
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
|
||||
* attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
|
||||
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
|
||||
* each package that serves no purpose other than being referenced by this attribute.
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
|
||||
/**
|
||||
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
|
||||
* {@link ReactiveNeo4jRepositoryFactoryBean}.
|
||||
*/
|
||||
Class<?> repositoryFactoryBeanClass() default ReactiveNeo4jRepositoryFactoryBean.class;
|
||||
|
||||
/**
|
||||
* Configure the repository base class to be used to create repository proxies for this particular configuration.
|
||||
*
|
||||
* @return The base class to be used when creating repository proxies.
|
||||
*/
|
||||
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected.
|
||||
*/
|
||||
String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.ReactiveNeo4jTemplate} bean to be used with the repositories detected.
|
||||
*/
|
||||
String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager} bean to be used with the repositories detected.
|
||||
*/
|
||||
String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
|
||||
* everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
|
||||
*/
|
||||
ComponentScan.Filter[] includeFilters() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are not eligible for component scanning.
|
||||
*/
|
||||
ComponentScan.Filter[] excludeFilters() default {};
|
||||
|
||||
/**
|
||||
* Configures the location of where to find the Spring Data named queries properties file. Will default to
|
||||
* {@code META-INFO/neo4j-named-queries.properties}.
|
||||
*/
|
||||
String namedQueriesLocation() default "";
|
||||
|
||||
/**
|
||||
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
|
||||
* for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
|
||||
* for {@code PersonRepositoryImpl}.
|
||||
*/
|
||||
String repositoryImplementationPostfix() default "Impl";
|
||||
|
||||
/**
|
||||
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
|
||||
* repositories infrastructure.
|
||||
*/
|
||||
boolean considerNestedRepositories() default false;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user