DATAGRAPH-1358 - Add support for configuring SDN 6 in a CDI context.

This brings in a new `Neo4jCdiExtension` as part of the `config` package being public API. 
This extension is loaded via Java SPI interface by CDI containers. It can also be used explicitly.

The extension is responsible for registering a producer of Neo4j related beans and also one `Neo4jRepositoryFactoryCdiBean` for each repository type discovered by Spring Data's CDI extension itself.

The task of the internal API `Neo4jRepositoryFactoryCdiBean` is getting the required beans from the CDI context and create the standard `Neo4jRepositoryFactory`, nothing more.

The new `Neo4jCdiConfigurationSupport` is an internal, application scoped API that produces a couple of required beans qualified as `Builtin` (a custom CDI qualifiers). Those can be overwritten by non-qualified beans of the same type by a user.

In addition to those changes, the PR also fixes the name of `AbstractNeo4jConfig#databaseSelectionProvider()` to bring symmetry to the API.

Unrelated to all of that, the `Neo4jConnectionSupport` in the `Neo4jExtension` has been fixed to not expose the volatile driver API, which caused the initial draft of `Neo4jCdiExtensionIT` to fail.
This commit is contained in:
Michael Simons
2020-08-11 17:28:33 +02:00
committed by GitHub
parent 34873fa444
commit b644817239
22 changed files with 886 additions and 22 deletions

17
pom.xml
View File

@@ -77,6 +77,7 @@
<changelist>-SNAPSHOT</changelist>
<checkstyle.version>8.29</checkstyle.version>
<cypher-dsl.version>2020.0.1</cypher-dsl.version>
<cdi>2.0</cdi>
<dist.id>spring-data-neo4j</dist.id>
<dist.key>SDNEO4J</dist.key>
<flatten-maven-plugin.version>1.2.1</flatten-maven-plugin.version>
@@ -406,6 +407,20 @@
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>${cdi}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>3.1.4.Final</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -641,7 +656,7 @@
<backend>html</backend>
<doctype>book</doctype>
<imagesDir>img</imagesDir>
<sourceDirectory>${basedir}/docs</sourceDirectory>
<sourceDirectory>${project.basedir}/src/main/asciidoc</sourceDirectory>
<sourceDocumentName>index.adoc</sourceDocumentName>
<sourceHighlighter>coderay</sourceHighlighter>
<attributes>

View File

@@ -3,6 +3,7 @@
Here are a couple of more frequently asked question in addition to the ones in the <<what-is-sdn,preface>>.
[[faq.multidatabase]]
== Neo4j 4.0 supports multiple databases - How can I use them?
You can either statically configure the database name or run your own database name provider.
@@ -32,6 +33,15 @@ Here is a working example for an imperative application secured with Spring Secu
[[faq.databaseSelectionProvider]]
.Neo4jConfig.java
----
import org.neo4j.springframework.data.core.DatabaseSelection;
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java[tags=faq.multidatabase]
----
@@ -208,3 +218,171 @@ movieExample = Example.of(
);
movies = this.movieRepository.findAll(movieExample);
----
== Do I need Spring Boot to use Spring Data Neo4j?
No, you don't.
While the automatic configuration of many Spring aspects through Spring Boot takes away a lot of manual cruft and is the recommended approach for setting up new Spring projects, you don't need to have to use this.
The following dependency is required for the solutions described above:
[source,xml,subs="verbatim,attributes"]
----
<dependency>
<groupId>{springGroupId}</groupId>
<artifactId>{artifactId}</artifactId>
<version>{spring-data-neo4j-version}</version>
</dependency>
----
The coordinates for a Gradle setup are the same.
To select a different database - either statically or dynamically - you can add a Bean of type `DatabaseSelectionProvider` as explained in <<faq.multidatabase>>.
For a reactive scenario, we provide `ReactiveDatabaseSelectionProvider`.
=== Using Spring Data Neo4j inside a Spring context without Spring Boot
We provide two abstract configuration classes to support you in bringing in the necessary beans: `AbstractNeo4jConfig` for imperative database access and `AbstractReactiveNeo4jConfig` for the reactive version.
They are meant to be used with `@EnableNeo4jRepositories` and `@EnableReactiveNeo4jRepositories` respectively.
See <<bootless-imperative-configuration>> and <<bootless-reactive-configuration>> for an example usage.
Both classes require you to override `driver()` in which you are supposed to create the driver.
To get the imperative version of the <<neo4j-client,Neo4j client>>, the template and support for imperative repositories, use something similar as shown here:
[source,java]
[[bootless-imperative-configuration]]
.Enabling Spring Data Neo4j infrastructure for imperative database access
----
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {
@Override @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());
}
@Override @Bean // <.>
protected DatabaseSelectionProvider databaseSelectionProvider() {
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
----
<.> The driver bean is required.
<.> This statically selects a database named `yourDatabase` and is *optional*.
The following listing provides the reactive Neo4j client and template, enables reactive transaction management and discovers Neo4j related repositories:
[source,java]
[[bootless-reactive-configuration]]
.Enabling Spring Data Neo4j infrastructure for reactive database access
----
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {
@Bean
@Override
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());
}
}
----
=== Using Spring Data Neo4j in a CDI 2.0 environment
For your convenience we provide a CDI extension with `Neo4jCdiExtension`.
When run in a compatible CDI 2.0 container, it will be automatically be registered and loaded through https://docs.oracle.com/javase/tutorial/ext/basics/spi.html[Java's service loader SPI].
The only thing you have to bring into your application is an annotated type that produces the Neo4j Java Driver:
[source,java]
[[cdi-driver-producer]]
.A CDI producer for the Neo4j Java Driver
----
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
public class Neo4jConfig {
@Produces @ApplicationScoped
public Driver driver() { // <.>
return GraphDatabase
.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
public void close(@Disposes Driver driver) {
driver.close();
}
@Produces @Singleton
public DatabaseSelectionProvider getDatabaseSelectionProvider() { // <.>
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
----
<.> Same as with plain Spring in <<bootless-imperative-configuration>>, but annotated with the corresponding CDI infrastructure.
<.> This is *optional*. However, if you run a custom database selection provider, you _must_ not qualify this bean.
If you are running in a SE Container - like the one https://weld.cdi-spec.org[Weld] provides for example, you can enable the extension like that:
[source,java]
[[cdi-driver-producer-se]]
.Enabling the Neo4j CDI extension in a SE container
----
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import org.springframework.data.neo4j.config.Neo4jCdiExtension;
public class SomeClass {
void someMethod() {
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addExtensions(Neo4jCdiExtension.class)
.addBeanClasses(YourDriverFactory.class)
.addPackages(Package.getPackage("your.domain.package"))
.initialize()
) {
SomeRepository someRepository = container.select(SomeRepository.class).get();
}
}
}
----

View File

@@ -24,7 +24,7 @@ include::{manualIncludeDir}/README.adoc[tags=properties]
:springVersion: 5.2.0.RELEASE
:spring-framework-docs: https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference
:spring-framework-javadoc: https://docs.spring.io/spring/docs/{springVersion}/javadoc-api
:spring-data-commons-docs: ../../../../../other-spring-data/spring-data-commons/src/main/asciidoc/
:spring-data-commons-docs: ../../../../../other-spring-data/spring-data-commons/src/main/asciidoc
(C) 2008-2020 The original authors.

View File

@@ -22,6 +22,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
@@ -60,7 +61,7 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
}
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public Neo4jTemplate neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext,
public Neo4jOperations neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext,
DatabaseSelectionProvider databaseNameProvider) {
return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
@@ -86,7 +87,7 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
* Neo4j 3.5 and prior.
*/
@Bean
protected DatabaseSelectionProvider neo4jDatabaseNameProvider() {
protected DatabaseSelectionProvider databaseSelectionProvider() {
return DatabaseSelectionProvider.getDefaultSelectionProvider();
}

View File

@@ -87,7 +87,7 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp
* Neo4j 3.5 and prior.
*/
@Bean
protected ReactiveDatabaseSelectionProvider reactiveNeo4jDatabaseNameProvider() {
protected ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider() {
return ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider();
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.config;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
import org.apiguardian.api.API;
/**
* An internally used CDI {@link Qualifier} to mark all beans produced by our
* {@link Neo4jCdiConfigurationSupport configuration support} as built in.
* When the {@link Neo4jCdiExtension Spring Data Neo4j CDI extension} is used,
* you can opt in to override any of the following beans by providing a {@link javax.enterprise.inject.Produces @Produces} method with the
* corresponding return type:
* <ul>
* <li>{@link org.springframework.data.neo4j.core.convert.Neo4jConversions}</li>
* <li>{@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}</li>
* <li>{@link org.springframework.data.neo4j.core.Neo4jOperations}</li>*
* </ul>
* The order in which the types are presented reflects the usefulness over overriding such a bean.
* You might want to add additional conversions to the mapping or provide a bean that dynamically selects a Neo4j database.
* Running a custom bean of the template or client might prove useful if you want to add additional methods.
*
* @author Michael J. Simons
* @soundtrack Buckethead - SIGIL Soundtrack
* @since 6.0
*/
@API(status = API.Status.STABLE, since = "6.0")
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Builtin {
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.config;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;
import org.apiguardian.api.API;
import org.neo4j.driver.Driver;
import org.springframework.data.mapping.callback.EntityCallback;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.event.BeforeBindCallback;
import org.springframework.data.neo4j.repository.event.IdGeneratingBeforeBindCallback;
import org.springframework.data.neo4j.repository.event.OptimisticLockingBeforeBindCallback;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Support class that can be used as is for all necessary CDI beans or as a blueprint for custom producers.
*
* @author Michael J. Simons
* @soundtrack Buckethead - SIGIL Soundtrack
* @since 6.0
*/
@API(status = API.Status.INTERNAL, since = "6.0")
@ApplicationScoped
class Neo4jCdiConfigurationSupport {
private <T> T resolve(Instance<T> instance) {
if (!instance.isAmbiguous()) {
return instance.get();
}
Instance<T> defaultInstance = instance.select(Neo4jCdiExtension.DEFAULT_BEAN);
return defaultInstance.get();
}
@Produces @Builtin @Singleton
public Neo4jConversions neo4jConversions() {
return new Neo4jConversions();
}
@Produces @Builtin @Singleton
public DatabaseSelectionProvider databaseSelectionProvider() {
return DatabaseSelectionProvider.getDefaultSelectionProvider();
}
@Produces @Builtin @Singleton
public Neo4jOperations neo4jOperations(
final @Any Instance<Neo4jClient> neo4jClient,
final @Any Instance<Neo4jMappingContext> mappingContext,
final @Any Instance<DatabaseSelectionProvider> databaseNameProvider,
final Instance<BeforeBindCallback> services
) {
EntityCallbacks entityCallbacks = EntityCallbacks.create(services.stream().toArray(EntityCallback[]::new));
return new Neo4jTemplate(resolve(neo4jClient), resolve(mappingContext), resolve(databaseNameProvider),
entityCallbacks);
}
@Produces @Singleton
public Neo4jClient neo4jClient(Driver driver) {
return Neo4jClient.create(driver);
}
@Produces @Singleton
public Neo4jMappingContext neo4jMappingContext(final Driver driver, final @Any Instance<Neo4jConversions> neo4JConversions) {
Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(resolve(neo4JConversions), driver.defaultTypeSystem());
return neo4jMappingContext;
}
@Produces @Singleton
public BeforeBindCallback<Object> idGeneratingBeforeBindCallback(
final @Any Instance<Neo4jMappingContext> mappingContext) {
return new IdGeneratingBeforeBindCallback(resolve(mappingContext));
}
@Produces @Singleton
public BeforeBindCallback<Object> optimisticLockingBeforeBindCallback(
final @Any Instance<Neo4jMappingContext> mappingContext) {
return new OptimisticLockingBeforeBindCallback(resolve(mappingContext));
}
@Produces @Singleton
public PlatformTransactionManager transactionManager(
Driver driver, @Any Instance<DatabaseSelectionProvider> databaseNameProvider) {
return new Neo4jTransactionManager(driver, resolve(databaseNameProvider));
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.config;
import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.util.AnnotationLiteral;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.springframework.core.log.LogAccessor;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryCdiBean;
import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
/**
* This CDI extension enables Spring Data Neo4j on a CDI 2.0 compatible CDI container. It creates a Neo4j client, template
* and brings in the Neo4j repository mechanism as well. It is the main entry point to our CDI support.
* <p/>
* It requires the presence of a Neo4j Driver bean. Other beans, like the {@link org.springframework.data.neo4j.core.convert.Neo4jConversions}
* can be overwritten by providing a producer of it. If such a producer or bean is added, it must not use any {@link javax.inject.Qualifier @Qualifier}
* on the bean.
* <p/>
* This CDI extension can be used either via a build in service loader mechanism or through building a context manually.
*
* @author Michael J. Simons
* @soundtrack Juse Ju - Millennium
* @since 6.0
*/
@API(status = API.Status.STABLE, since = "6.0")
public final class Neo4jCdiExtension extends CdiRepositoryExtensionSupport {
/**
* An annotation literal used for selecting default CDI beans.
*/
public static final AnnotationLiteral<Default> DEFAULT_BEAN = new AnnotationLiteral<Default>() {
@Override public Class<? extends Annotation> annotationType() {
return Default.class;
}
};
/**
* An annotation literal used for selecting {@link Any @Any} annotated beans.
*/
public static final AnnotationLiteral<Any> ANY_BEAN = new AnnotationLiteral<Any>() {
@Override public Class<? extends Annotation> annotationType() {
return Any.class;
}
};
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jCdiExtension.class));
public Neo4jCdiExtension() {
log.info("Activating CDI extension for Spring Data Neo4j repositories.");
}
void addNeo4jBeansProducer(@Observes BeforeBeanDiscovery event) {
event.addAnnotatedType(Neo4jCdiConfigurationSupport.class, "Neo4jCDIConfigurationSupport");
}
void registerRepositoryFactoryBeanPerRepositoryType(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
Optional<CustomRepositoryImplementationDetector> optionalCustomRepositoryImplementationDetector =
Optional.ofNullable(getCustomImplementationDetector());
for (Map.Entry<Class<?>, Set<Annotation>> entry : getRepositoryTypes()) {
Class<?> repositoryType = entry.getKey();
Set<Annotation> qualifiers = entry.getValue();
Neo4jRepositoryFactoryCdiBean<?> repositoryBean = new Neo4jRepositoryFactoryCdiBean<>(
qualifiers,
repositoryType,
beanManager,
optionalCustomRepositoryImplementationDetector
);
registerBean(repositoryBean);
event.addBean(repositoryBean);
}
}
}

View File

@@ -93,6 +93,12 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext,
DatabaseSelectionProvider databaseSelectionProvider) {
this(neo4jClient, neo4jMappingContext, databaseSelectionProvider, EntityCallbacks.create());
}
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext,
DatabaseSelectionProvider databaseSelectionProvider, EntityCallbacks entityCallbacks) {
Assert.notNull(neo4jClient, "The Neo4jClient is required");
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
Assert.notNull(databaseSelectionProvider, "The database name provider is required");
@@ -100,7 +106,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
this.neo4jClient = neo4jClient;
this.neo4jMappingContext = neo4jMappingContext;
this.cypherGenerator = CypherGenerator.INSTANCE;
this.eventSupport = new Neo4jEvents(EntityCallbacks.create());
this.eventSupport = new Neo4jEvents(entityCallbacks);
this.databaseSelectionProvider = databaseSelectionProvider;
}

View File

@@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apiguardian.api.API;
import org.neo4j.driver.Driver;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -82,9 +83,27 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
this(neo4jConversions, null);
}
/**
* This API is primarly used from inside the CDI extension to configure the type system. This is necessary as
* we don't get notified of the context via {@link #setApplicationContext(ApplicationContext applicationContext)}.
*
* @param neo4jConversions The conversions to be used
* @param typeSystem The current drivers typeystem
*/
@API(status = API.Status.INTERNAL, since = "6.0")
public Neo4jMappingContext(Neo4jConversions neo4jConversions, TypeSystem typeSystem) {
super.setSimpleTypeHolder(Neo4jSimpleTypes.HOLDER);
this.neo4jConversions = neo4jConversions;
this.converter = new DefaultNeo4jConverter(neo4jConversions, nodeDescriptionStore);
DefaultNeo4jConverter defaultNeo4jConverter = new DefaultNeo4jConverter(neo4jConversions, nodeDescriptionStore);
if (typeSystem != null) {
defaultNeo4jConverter.setTypeSystem(typeSystem);
}
this.converter = defaultNeo4jConverter;
}
public Neo4jConverter getConverter() {

View File

@@ -37,7 +37,6 @@ public final class IdGeneratingBeforeBindCallback implements BeforeBindCallback<
@Override
public Object onBeforeBind(Object entity) {
return idPopulator.populateIfNecessary(entity);
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.support;
import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.config.Neo4jCdiExtension;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.repository.cdi.CdiRepositoryBean;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
/**
* The CDI pendant to the {@link Neo4jRepositoryFactoryBean}. It creates instances of {@link Neo4jRepositoryFactory}.
*
* @param <T> The type of the repository being created
* @author Michael J. Simons
* @soundtrack Various - TRON Legacy R3conf1gur3d
* @since 6.0
*/
@API(status = API.Status.INTERNAL, since = "6.0")
public final class Neo4jRepositoryFactoryCdiBean<T> extends CdiRepositoryBean<T> {
private final BeanManager beanManager;
public Neo4jRepositoryFactoryCdiBean(Set<Annotation> qualifiers, Class<T> repositoryType,
BeanManager beanManager, Optional<CustomRepositoryImplementationDetector> detector) {
super(qualifiers, repositoryType, beanManager, detector);
this.beanManager = beanManager;
}
@Override
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
Neo4jOperations neo4jOperations = getReference(Neo4jOperations.class, creationalContext);
Neo4jMappingContext mappingContext = getReference(Neo4jMappingContext.class, creationalContext);
return create(() -> new Neo4jRepositoryFactory(neo4jOperations, mappingContext), repositoryType);
}
private <T> T getReference(Class<T> clazz, CreationalContext<?> creationalContext) {
Set<Bean<?>> beans = beanManager.getBeans(clazz, Neo4jCdiExtension.ANY_BEAN);
if (beans.size() > 1) {
beans = beans.stream()
.filter(b -> b.getQualifiers().contains(Neo4jCdiExtension.DEFAULT_BEAN))
.collect(Collectors.toSet());
}
Bean<?> bean = beanManager.resolve(beans);
return (T) beanManager.getReference(bean, clazz, creationalContext);
}
}

View File

@@ -0,0 +1 @@
org.springframework.data.neo4j.config.Neo4jCdiExtension

View File

@@ -54,14 +54,6 @@ class User {
}
}
// tag::faq.multidatabase[]
// end::faq.multidatabase[]
// tag::faq.multidatabase[]
// end::faq.multidatabase[]
// tag::faq.multidatabase[]
// end::faq.multidatabase[]
/**
* @author Michael J. Simons
*/

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.cdi;
import javax.inject.Inject;
import org.neo4j.driver.Driver;
/**
* @author Michael J. Simons
* @soundtrack Various - TRON Legacy R3conf1gur3d
*/
class Neo4jBasedService {
final Driver driver;
final PersonRepository personRepository;
@Inject
Neo4jBasedService(Driver driver, PersonRepository personRepository) {
this.driver = driver;
this.personRepository = personRepository;
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.cdi;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.AmbiguousResolutionException;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import javax.inject.Singleton;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.neo4j.driver.Driver;
import org.springframework.data.neo4j.config.Neo4jCdiExtension;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.test.Neo4jExtension;
/**
* @author Michael J. Simons
* @soundtrack Various - TRON Legacy R3conf1gur3d
*/
@ExtendWith(Neo4jExtension.class)
class Neo4jCdiExtensionIT {
protected static Neo4jExtension.Neo4jConnectionSupport connectionSupport;
@ApplicationScoped
static class RealDriverFactory {
@Produces
@Singleton
public Driver driver() {
return connectionSupport.getDriver();
}
}
@ApplicationScoped
static class MockedDriverFactory {
@Produces
@Singleton
public Driver driver() {
return Mockito.mock(Driver.class);
}
}
@ApplicationScoped
static class CustomDependencyProducer {
Neo4jConversions conversions = Mockito.mock(Neo4jConversions.class);
DatabaseSelectionProvider databaseSelectionProvider = Mockito.mock(DatabaseSelectionProvider.class);
Neo4jOperations neo4jOperations = Mockito.mock(Neo4jOperations.class);
@Produces @Singleton
public Neo4jConversions getConversions() {
return conversions;
}
@Produces @Singleton
public DatabaseSelectionProvider getDatabaseSelectionProvider() {
return databaseSelectionProvider;
}
@Produces @Singleton
public Neo4jOperations getNeo4jOperations() {
return neo4jOperations;
}
}
@ApplicationScoped
static class BrokenCustomDependencyProducer {
@Produces @Singleton
public Neo4jConversions getConversions1() {
return Mockito.mock(Neo4jConversions.class);
}
@Produces @Singleton
public Neo4jConversions getConversions2() {
return Mockito.mock(Neo4jConversions.class);
}
}
@Test
void cdiExtensionShouldProduceFunctionalRepositories() {
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addExtensions(Neo4jCdiExtension.class)
.addBeanClasses(RealDriverFactory.class, PersonRepository.class, Neo4jBasedService.class)
.initialize()) {
Neo4jBasedService client = container
.select(Neo4jBasedService.class).get();
assertThat(client).isNotNull();
assertThat(client.driver).isNotNull();
assertThat(client.personRepository).isNotNull();
Person p = client.personRepository.save(new Person("Hello"));
assertThat(p.getId()).isNotNull();
Optional<Person> loadedPerson = client.personRepository.findById(p.getId());
assertThat(loadedPerson).isPresent().hasValueSatisfying(v -> v.getId().equals(p.getId()));
}
}
@Test
void shouldAllowToOverrideASetOfDependents() {
Class<?> configurationSupport = getNeo4jCdiConfigurationSupport();
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addBeanClasses(
MockedDriverFactory.class,
CustomDependencyProducer.class,
configurationSupport
)
.initialize()) {
CustomDependencyProducer customDependencyProducer = container.select(CustomDependencyProducer.class).get();
assertThat(container.select(Neo4jConversions.class).get())
.isEqualTo(customDependencyProducer.getConversions());
assertThat(container.select(DatabaseSelectionProvider.class).get())
.isEqualTo(customDependencyProducer.getDatabaseSelectionProvider());
assertThat(container.select(Neo4jOperations.class).get())
.isEqualTo(customDependencyProducer.getNeo4jOperations());
}
}
@Test
void shouldRequireUniqueDefaultBeans() {
Class<?> configurationSupport = getNeo4jCdiConfigurationSupport();
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addBeanClasses(
MockedDriverFactory.class,
BrokenCustomDependencyProducer.class,
configurationSupport
)
.initialize()) {
assertThatExceptionOfType(AmbiguousResolutionException.class).isThrownBy(() -> {
Neo4jMappingContext context = container.select(Neo4jMappingContext.class).get();
});
}
}
private Class<?> getNeo4jCdiConfigurationSupport() {
try {
// Wrapped in a reflection call so that we don't need to make it public just
// for testing it's producer methods.
return Class.forName("org.springframework.data.neo4j.config.Neo4jCdiConfigurationSupport");
} catch (ClassNotFoundException e) {
throw new RuntimeException("¯\\_(ツ)_/¯", e);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.cdi;
import java.time.LocalDate;
import java.util.UUID;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
/**
* This domain object features a client side generated ID on purpose. It is needed to verify that the callbacks generating
* those are actually registered correct.
*
* @author Michael J. Simons
* @soundtrack Various - TRON Legacy R3conf1gur3d
*/
@Node
class Person {
@Id @GeneratedValue
private UUID id;
@CreatedDate
private LocalDate createdAt;
private final String name;
Person(String name) {
this.name = name;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public LocalDate getCreatedAt() {
return createdAt;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.cdi;
import java.util.UUID;
import org.springframework.data.neo4j.repository.Neo4jRepository;
/**
* @author Michael J. Simons
* @soundtrack Various - TRON Legacy R3conf1gur3d
*/
interface PersonRepository extends Neo4jRepository<Person, UUID> {
}

View File

@@ -41,7 +41,7 @@ class RepositoryWithADifferentDatabaseIT extends RepositoryIT {
@BeforeAll
static void createTestDatabase() {
try (Session session = neo4jConnectionSupport.driverInstance.session(SessionConfig.forDatabase("system"))) {
try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) {
session.run("CREATE DATABASE " + TEST_DATABASE_NAME).consume();
}
@@ -50,7 +50,7 @@ class RepositoryWithADifferentDatabaseIT extends RepositoryIT {
@AfterAll
static void dropTestDatabase() {
try (Session session = neo4jConnectionSupport.driverInstance.session(SessionConfig.forDatabase("system"))) {
try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) {
session.run("DROP DATABASE " + TEST_DATABASE_NAME).consume();
}

View File

@@ -2220,7 +2220,7 @@ class ReactiveRepositoryIT {
@Override
@Bean
protected ReactiveDatabaseSelectionProvider reactiveNeo4jDatabaseNameProvider() {
protected ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider() {
return Optional.ofNullable(databaseSelection.getValue())
.map(ReactiveDatabaseSelectionProvider::createStaticDatabaseSelectionProvider)
.orElse(ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider());

View File

@@ -41,7 +41,7 @@ class ReactiveRepositoryWithADifferentDatabaseIT extends ReactiveRepositoryIT {
@BeforeAll
static void createTestDatabase() {
try (Session session = neo4jConnectionSupport.driverInstance.session(SessionConfig.forDatabase("system"))) {
try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) {
session.run("CREATE DATABASE " + TEST_DATABASE_NAME).consume();
}
@@ -50,7 +50,7 @@ class ReactiveRepositoryWithADifferentDatabaseIT extends ReactiveRepositoryIT {
@AfterAll
static void dropTestDatabase() {
try (Session session = neo4jConnectionSupport.driverInstance.session(SessionConfig.forDatabase("system"))) {
try (Session session = neo4jConnectionSupport.getDriver().session(SessionConfig.forDatabase("system"))) {
session.run("DROP DATABASE " + TEST_DATABASE_NAME).consume();
}

View File

@@ -163,7 +163,7 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback {
/**
* Shared instance of the standard (non-routing) driver.
*/
public volatile Driver driverInstance;
private volatile Driver driverInstance;
public Neo4jConnectionSupport(String url, AuthToken authToken) {
this.url = url;