Merge SDN/RX into SDN.

This commit is contained in:
Michael Simons
2020-07-02 10:15:10 +02:00
319 changed files with 42439 additions and 589 deletions

40
.gitignore vendored
View File

@@ -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
View 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();
}
}

View File

@@ -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

View File

@@ -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? Wed 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 doesnt 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 dont 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
View 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
View 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
View 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
View 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.

View 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
View 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>

View 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 .+;$
^.*$

View 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
View 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
----

View 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[]

View 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
----

View 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
View File

@@ -0,0 +1,2 @@
lombok.nonNull.exceptionType = IllegalArgumentException

661
pom.xml
View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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 "";
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Neo4j-specific conversion classes.
*/
@NonNullApi
package org.neo4j.springframework.data.core.convert;
import org.springframework.lang.NonNullApi;

View File

@@ -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();
}
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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&lt;String&gt;} containing dynamic "runtime managed" labels.
*/
Optional<Neo4jPersistentProperty> getDynamicLabelsProperty();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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&lt;String&gt;}. 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 {
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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>
* &#64;Node
* public class MyEntity {
* &#64;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>
* &#64;Node
* public class MyEntity {
* &#64;Id &#64;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>
* &#64;Node
* public class MyEntity {
* &#64;Id &#64;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 {
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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 "";
}

View File

@@ -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();
}
}

View File

@@ -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 "";
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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 {
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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() {
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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()));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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