From b644817239da235ea359d3440422467c4135fe8e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 11 Aug 2020 17:28:33 +0200 Subject: [PATCH] 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. --- pom.xml | 17 +- src/main/asciidoc/faq/faq.adoc | 178 +++++++++++++++++ src/main/asciidoc/index.adoc | 2 +- .../neo4j/config/AbstractNeo4jConfig.java | 5 +- .../config/AbstractReactiveNeo4jConfig.java | 2 +- .../data/neo4j/config/Builtin.java | 50 +++++ .../config/Neo4jCdiConfigurationSupport.java | 116 +++++++++++ .../data/neo4j/config/Neo4jCdiExtension.java | 103 ++++++++++ .../data/neo4j/core/Neo4jTemplate.java | 8 +- .../core/mapping/Neo4jMappingContext.java | 21 +- .../event/IdGeneratingBeforeBindCallback.java | 1 - .../Neo4jRepositoryFactoryCdiBean.java | 75 +++++++ .../javax.enterprise.inject.spi.Extension | 1 + .../data/neo4j/documentation/Neo4jConfig.java | 8 - .../integration/cdi/Neo4jBasedService.java | 37 ++++ .../integration/cdi/Neo4jCdiExtensionIT.java | 185 ++++++++++++++++++ .../data/neo4j/integration/cdi/Person.java | 60 ++++++ .../integration/cdi/PersonRepository.java | 27 +++ .../RepositoryWithADifferentDatabaseIT.java | 4 +- .../reactive/ReactiveRepositoryIT.java | 2 +- ...iveRepositoryWithADifferentDatabaseIT.java | 4 +- .../data/neo4j/test/Neo4jExtension.java | 2 +- 22 files changed, 886 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/springframework/data/neo4j/config/Builtin.java create mode 100644 src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java create mode 100644 src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java create mode 100644 src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java create mode 100644 src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension create mode 100644 src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java diff --git a/pom.xml b/pom.xml index 38818da04..54c4641de 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ -SNAPSHOT 8.29 2020.0.1 + 2.0 spring-data-neo4j SDNEO4J 1.2.1 @@ -406,6 +407,20 @@ jackson-databind test + + + javax.enterprise + cdi-api + ${cdi} + provided + + + org.jboss.weld.se + weld-se-core + 3.1.4.Final + test + + @@ -641,7 +656,7 @@ html book img - ${basedir}/docs + ${project.basedir}/src/main/asciidoc index.adoc coderay diff --git a/src/main/asciidoc/faq/faq.adoc b/src/main/asciidoc/faq/faq.adoc index 4ee7957f1..bd4c90cfe 100644 --- a/src/main/asciidoc/faq/faq.adoc +++ b/src/main/asciidoc/faq/faq.adoc @@ -3,6 +3,7 @@ Here are a couple of more frequently asked question in addition to the ones in the <>. +[[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"] +---- + + {springGroupId} + {artifactId} + {spring-data-neo4j-version} + +---- + +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 <>. +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 <> and <> 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 <>, 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 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 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 <>, 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(); + } + } +} +---- \ No newline at end of file diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 129e6156d..364b14278 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -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. diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java index 2b7bd91b1..e25089a00 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java @@ -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(); } diff --git a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java index aa8e4a6d6..a77072fcd 100644 --- a/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java +++ b/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java @@ -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(); } diff --git a/src/main/java/org/springframework/data/neo4j/config/Builtin.java b/src/main/java/org/springframework/data/neo4j/config/Builtin.java new file mode 100644 index 000000000..df2cea8e8 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/config/Builtin.java @@ -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: + *
    + *
  • {@link org.springframework.data.neo4j.core.convert.Neo4jConversions}
  • + *
  • {@link org.springframework.data.neo4j.core.DatabaseSelectionProvider}
  • + *
  • {@link org.springframework.data.neo4j.core.Neo4jOperations}
  • * + *
+ * The order in which the types are presented reflects the usefulness over overriding such a bean. + * You might want to add additional conversions to the mapping or provide a bean that dynamically selects a Neo4j database. + * Running a custom bean of the template or client might prove useful if you want to add additional methods. + * + * @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 { +} diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java new file mode 100644 index 000000000..51fd1d1ef --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiConfigurationSupport.java @@ -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 resolve(Instance instance) { + if (!instance.isAmbiguous()) { + return instance.get(); + } + + Instance 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, + final @Any Instance mappingContext, + final @Any Instance databaseNameProvider, + final Instance 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) { + + Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(resolve(neo4JConversions), driver.defaultTypeSystem()); + return neo4jMappingContext; + } + + @Produces @Singleton + public BeforeBindCallback idGeneratingBeforeBindCallback( + final @Any Instance mappingContext) { + + return new IdGeneratingBeforeBindCallback(resolve(mappingContext)); + } + + @Produces @Singleton + public BeforeBindCallback optimisticLockingBeforeBindCallback( + final @Any Instance mappingContext) { + + return new OptimisticLockingBeforeBindCallback(resolve(mappingContext)); + } + + @Produces @Singleton + public PlatformTransactionManager transactionManager( + Driver driver, @Any Instance databaseNameProvider) { + + return new Neo4jTransactionManager(driver, resolve(databaseNameProvider)); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java new file mode 100644 index 000000000..3d238ca7a --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/config/Neo4jCdiExtension.java @@ -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. + *

+ * 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. + *

+ * 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_BEAN = new AnnotationLiteral() { + @Override public Class annotationType() { + return Default.class; + } + }; + /** + * An annotation literal used for selecting {@link Any @Any} annotated beans. + */ + public static final AnnotationLiteral ANY_BEAN = new AnnotationLiteral() { + @Override public Class annotationType() { + 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 optionalCustomRepositoryImplementationDetector = + Optional.ofNullable(getCustomImplementationDetector()); + + for (Map.Entry, Set> entry : getRepositoryTypes()) { + + Class repositoryType = entry.getKey(); + Set qualifiers = entry.getValue(); + + Neo4jRepositoryFactoryCdiBean repositoryBean = new Neo4jRepositoryFactoryCdiBean<>( + qualifiers, + repositoryType, + beanManager, + optionalCustomRepositoryImplementationDetector + ); + + registerBean(repositoryBean); + event.addBean(repositoryBean); + } + } +} diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index b25613a6a..db87aa67c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -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; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 7474e0bcd..dcef2eb34 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -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 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 extends CdiRepositoryBean { + + private final BeanManager beanManager; + + public Neo4jRepositoryFactoryCdiBean(Set qualifiers, Class repositoryType, + BeanManager beanManager, Optional detector) { + super(qualifiers, repositoryType, beanManager, detector); + + this.beanManager = beanManager; + } + + @Override + protected T create(CreationalContext creationalContext, Class repositoryType) { + + Neo4jOperations neo4jOperations = getReference(Neo4jOperations.class, creationalContext); + Neo4jMappingContext mappingContext = getReference(Neo4jMappingContext.class, creationalContext); + + return create(() -> new Neo4jRepositoryFactory(neo4jOperations, mappingContext), repositoryType); + } + + private T getReference(Class clazz, CreationalContext creationalContext) { + + Set> 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); + } +} diff --git a/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 000000000..a8ea79a36 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.springframework.data.neo4j.config.Neo4jCdiExtension diff --git a/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java b/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java index 1106e4a4f..0e6e5d2de 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java @@ -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 */ diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java new file mode 100644 index 000000000..3a3dc22c7 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jBasedService.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java new file mode 100644 index 000000000..b17a70b32 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java @@ -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 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); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java new file mode 100644 index 000000000..a800c5c0b --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Person.java @@ -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; + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java new file mode 100644 index 000000000..2a043382f --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/PersonRepository.java @@ -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 { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java index 506c4d8fd..d44283113 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryWithADifferentDatabaseIT.java @@ -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(); } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index 6fea81d86..e4454aab9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -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()); diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java index 63adf3e9b..4f53eb105 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryWithADifferentDatabaseIT.java @@ -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(); } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index 9ba8dc964..6e4874510 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -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;