diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java new file mode 100644 index 0000000..2494a0c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016-2018 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 + * + * http://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.r2dbc.repository.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.beans.factory.FactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; + +/** + * Annotation to activate reactive relational repositories using R2DBC. If no base package is configured through either + * {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of + * annotated class. + * + * @author Mark Paluch + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(R2dbcRepositoriesRegistrar.class) +public @interface EnableR2dbcRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableReactiveRelationalRepositories("org.my.pkg")} instead of + * {@code @EnableReactiveRelationalRepositories(basePackages="org.my.pkg")}. + */ + 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. + */ + 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 {}; + + /** + * 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. + */ + Filter[] includeFilters() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + Filter[] excludeFilters() 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}. + * + * @return + */ + String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the location of where to find the Spring Data named queries properties file. Will default to + * {@code META-INF/r2dbc-named-queries.properties}. + * + * @return + */ + String namedQueriesLocation() default ""; + + /** + * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to + * {@link Key#CREATE_IF_NOT_FOUND}. + * + * @return + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to + * {@link R2dbcRepositoryFactoryBean}. + * + * @return + */ + Class repositoryFactoryBeanClass() default R2dbcRepositoryFactoryBean.class; + + /** + * Configure the repository base class to be used to create repository proxies for this particular configuration. + * + * @return + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * Configures the name of the {@link org.springframework.data.r2dbc.function.DatabaseClient} bean to be used with the + * repositories detected. + * + * @return + */ + String databaseClientRef() default "databaseClient"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the + * repositories infrastructure. + */ + boolean considerNestedRepositories() default false; +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java new file mode 100644 index 0000000..8e5156d --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java @@ -0,0 +1,33 @@ +package org.springframework.data.r2dbc.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * R2DBC-specific {@link org.springframework.context.annotation.ImportBeanDefinitionRegistrar}. + * + * @author Mark Paluch + * @since 2.0 + */ +class R2dbcRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableR2dbcRepositories.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() + */ + @Override + protected RepositoryConfigurationExtension getExtension() { + return new R2dbcRepositoryConfigurationExtension(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java new file mode 100644 index 0000000..221124e --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016-2018 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 + * + * http://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.r2dbc.repository.config; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryMetadata; + +/** + * Reactive {@link RepositoryConfigurationExtension} for R2DBC. + * + * @author Mark Paluch + */ +public class R2dbcRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModuleName() + */ + @Override + public String getModuleName() { + return "R2DBC"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return "r2dbc"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() + */ + public String getRepositoryFactoryBeanClassName() { + return R2dbcRepositoryFactoryBean.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations() + */ + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.singleton(Table.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingTypes() + */ + @Override + protected Collection> getIdentifyingTypes() { + return Collections.singleton(R2dbcRepository.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) {} + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { + + AnnotationAttributes attributes = config.getAttributes(); + + builder.addPropertyReference("databaseClient", attributes.getString("databaseClientRef")); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#useRepositoryConfiguration(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + return metadata.isReactiveRepository(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java new file mode 100644 index 0000000..054730d --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 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 + * + * http://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.r2dbc.repository.support; + +import java.io.Serializable; + +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.FactoryBean} to create + * {@link org.springframework.data.r2dbc.repository.R2dbcRepository} instances. + * + * @author Mark Paluch + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository + */ +public class R2dbcRepositoryFactoryBean, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport { + + private @Nullable DatabaseClient client; + private @Nullable MappingContext, RelationalPersistentProperty> mappingContext; + + private boolean mappingContextConfigured = false; + + /** + * Creates a new {@link R2dbcRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public R2dbcRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + /** + * Configures the {@link DatabaseClient} to be used. + * + * @param client the client to set + */ + public void setDatabaseClient(@Nullable DatabaseClient client) { + this.client = client; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext) + */ + @Override + @SuppressWarnings("unchecked") + protected void setMappingContext(MappingContext mappingContext) { + + super.setMappingContext(mappingContext); + this.mappingContext = (MappingContext, RelationalPersistentProperty>) mappingContext; + this.mappingContextConfigured = true; + } + + @Override + protected final RepositoryFactorySupport createRepositoryFactory() { + return getFactoryInstance(client, this.mappingContext); + } + + /** + * Creates and initializes a {@link RepositoryFactorySupport} instance. + * + * @param client + * @param mappingContext + * @return + */ + protected RepositoryFactorySupport getFactoryInstance(DatabaseClient client, + MappingContext, RelationalPersistentProperty> mappingContext) { + return new R2dbcRepositoryFactory(client, mappingContext); + } + + @Override + public void afterPropertiesSet() { + + Assert.state(client != null, "DatabaseClient must not be null!"); + + if (!mappingContextConfigured) { + setMappingContext(new RelationalMappingContext()); + } + + super.afterPropertiesSet(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java new file mode 100644 index 0000000..033b897 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java @@ -0,0 +1,6 @@ +package org.springframework.data.r2dbc.repository.config; + +/** + * @author Mark Paluch + */ +class Person {} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java new file mode 100644 index 0000000..d10ba35 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java @@ -0,0 +1,8 @@ +package org.springframework.data.r2dbc.repository.config; + +import org.springframework.data.r2dbc.repository.R2dbcRepository; + +/** + * @author Mark Paluch + */ +interface PersonRepository extends R2dbcRepository {} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java new file mode 100644 index 0000000..6bb3023 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2018 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 + * + * http://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.r2dbc.repository.config; + +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link R2dbcRepositoriesRegistrar}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class R2dbcRepositoriesRegistrarTests { + + @Configuration + @EnableR2dbcRepositories(basePackages = "org.springframework.data.r2dbc.repository.config") + static class Config { + + @Bean + public DatabaseClient databaseClient() { + return mock(DatabaseClient.class); + } + } + + @Autowired PersonRepository personRepository; + @Autowired ApplicationContext context; + + @Test // gh-13 + public void testConfiguration() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java new file mode 100644 index 0000000..c904371 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016-2018 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 + * + * http://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.r2dbc.repository.config; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.Test; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfiguration; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +/** + * Unit tests for {@link R2dbcRepositoryConfigurationExtension}. + * + * @author Mark Paluch + */ +public class R2dbcRepositoryConfigurationExtensionUnitTests { + + StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); + + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableR2dbcRepositories.class, loader, environment, registry); + + @Test // gh-13 + public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { + + R2dbcRepositoryConfigurationExtension extension = new R2dbcRepositoryConfigurationExtension(); + assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + @Test // gh-13 + public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + + R2dbcRepositoryConfigurationExtension extension = new R2dbcRepositoryConfigurationExtension(); + assertHasRepo(StoreRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + @Test // gh-13 + public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { + + R2dbcRepositoryConfigurationExtension extension = new R2dbcRepositoryConfigurationExtension(); + assertDoesNotHaveRepo(UnannotatedRepository.class, + extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + private static void assertHasRepo(Class repositoryInterface, + Collection> configs) { + + for (RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + return; + } + } + + fail("Expected to find config for repository interface ".concat(repositoryInterface.getName()).concat(" but got ") + .concat(configs.toString())); + } + + private static void assertDoesNotHaveRepo(Class repositoryInterface, + Collection> configs) { + + for (RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + fail("Expected not to find config for repository interface ".concat(repositoryInterface.getName())); + } + } + } + + @EnableR2dbcRepositories(considerNestedRepositories = true) + static class Config {} + + @Table("sample") + static class Sample {} + + static class Store {} + + interface SampleRepository extends ReactiveCrudRepository {} + + interface UnannotatedRepository extends ReactiveCrudRepository {} + + interface StoreRepository extends R2dbcRepository {} +}