diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java index 2969cbc12..8bddf7dfd 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java @@ -15,33 +15,26 @@ */ package org.springframework.data.repository.cdi; +import lombok.Getter; import lombok.RequiredArgsConstructor; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.Optional; import java.util.stream.Stream; -import javax.enterprise.inject.CreationException; import javax.enterprise.inject.UnsatisfiedResolutionException; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; -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.ClassMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; import org.springframework.data.repository.config.FragmentMetadata; +import org.springframework.data.repository.config.ImplementationDetectionConfiguration; +import org.springframework.data.repository.config.ImplementationLookupConfiguration; import org.springframework.data.repository.config.RepositoryFragmentConfiguration; -import org.springframework.data.repository.config.RepositoryFragmentDiscovery; import org.springframework.data.util.Optionals; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; @@ -61,6 +54,7 @@ public class CdiRepositoryContext { private final ClassLoader classLoader; private final CustomRepositoryImplementationDetector detector; private final MetadataReaderFactory metadataReaderFactory; + private final FragmentMetadata metdata; /** * Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize @@ -69,16 +63,8 @@ public class CdiRepositoryContext { * @param classLoader must not be {@literal null}. */ public CdiRepositoryContext(ClassLoader classLoader) { - - Assert.notNull(classLoader, "ClassLoader must not be null!"); - - this.classLoader = classLoader; - - Environment environment = new StandardEnvironment(); - ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader); - - this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); - this.detector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader); + this(classLoader, new CustomRepositoryImplementationDetector(new StandardEnvironment(), + new PathMatchingResourcePatternResolver(classLoader))); } /** @@ -97,6 +83,7 @@ public class CdiRepositoryContext { this.classLoader = classLoader; this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.metdata = new FragmentMetadata(metadataReaderFactory); this.detector = detector; } @@ -130,14 +117,11 @@ public class CdiRepositoryContext { Stream getRepositoryFragments(CdiRepositoryConfiguration configuration, Class repositoryInterface) { - ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName()); + CdiImplementationDetectionConfiguration config = new CdiImplementationDetectionConfiguration(configuration, + metadataReaderFactory); - RepositoryFragmentDiscovery fragmentConfiguration = new CdiRepositoryFragmentDiscovery(configuration); - - return Arrays.stream(classMetadata.getInterfaceNames()) // - .filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) // - .map(it -> FragmentMetadata.of(it, fragmentConfiguration)) // - .map(this::detectRepositoryFragmentConfiguration) // + return metdata.getFragmentInterfaces(repositoryInterface.getName()) // + .map(it -> detectRepositoryFragmentConfiguration(it, config)) // .flatMap(Optionals::toStream); } @@ -152,26 +136,22 @@ public class CdiRepositoryContext { Optional> getCustomImplementationClass(Class repositoryType, CdiRepositoryConfiguration cdiRepositoryConfiguration) { - String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration); + ImplementationDetectionConfiguration configuration = new CdiImplementationDetectionConfiguration( + cdiRepositoryConfiguration, metadataReaderFactory); + ImplementationLookupConfiguration lookup = configuration.forFragment(repositoryType.getName()); - Optional beanDefinition = detector.detectCustomImplementation( // - className, // - className, Collections.singleton(repositoryType.getPackage().getName()), // - Collections.emptySet(), // - BeanDefinition::getBeanClassName); + Optional beanDefinition = detector.detectCustomImplementation(lookup); return beanDefinition.map(this::loadBeanClass); } - private Optional detectRepositoryFragmentConfiguration( - FragmentMetadata configuration) { + private Optional detectRepositoryFragmentConfiguration(String fragmentInterfaceName, + CdiImplementationDetectionConfiguration config) { - String className = configuration.getFragmentImplementationClassName(); + ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterfaceName); + Optional beanDefinition = detector.detectCustomImplementation(lookup); - Optional beanDefinition = detector.detectCustomImplementation(className, null, - configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName); - - return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); + return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterfaceName, bd)); } @Nullable @@ -182,45 +162,37 @@ public class CdiRepositoryContext { return beanClassName == null ? null : loadClass(beanClassName); } - private static ClassMetadata getClassMetadata(MetadataReaderFactory metadataReaderFactory, String className) { - - try { - return metadataReaderFactory.getMetadataReader(className).getClassMetadata(); - } catch (IOException e) { - throw new CreationException(String.format("Cannot parse %s metadata.", className), e); - } - } - - private static String getCustomImplementationClassName(Class repositoryType, - CdiRepositoryConfiguration cdiRepositoryConfiguration) { - - String configuredPostfix = cdiRepositoryConfiguration.getRepositoryImplementationPostfix(); - Assert.hasText(configuredPostfix, "Configured repository postfix must not be null or empty!"); - - return ClassUtils.getShortName(repositoryType) + configuredPostfix; - } - @RequiredArgsConstructor - private static class CdiRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery { + private static class CdiImplementationDetectionConfiguration implements ImplementationDetectionConfiguration { private final CdiRepositoryConfiguration configuration; + private final @Getter MetadataReaderFactory metadataReaderFactory; /* * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters() + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix() + */ + @Override + public String getImplementationPostfix() { + return configuration.getRepositoryImplementationPostfix(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return Streamable.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters() */ @Override public Streamable getExcludeFilters() { - return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix() - */ - @Override - public Optional getRepositoryImplementationPostfix() { - return Optional.of(configuration.getRepositoryImplementationPostfix()); + return Streamable.empty(); } } } diff --git a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java index f364ca285..d30b925b2 100644 --- a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java +++ b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java @@ -15,29 +15,27 @@ */ package org.springframework.data.repository.config; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - import java.util.Collection; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; -import javax.annotation.Nullable; - import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.data.util.Streamable; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; /** - * Detects the custom implementation for a {@link org.springframework.data.repository.Repository} + * Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If + * configured with a {@link ImplementationDetectionConfiguration} at construction time, the necessary component scan is + * executed on first access, cached and its result is the filtered on every further implementation lookup according to + * the given {@link ImplementationDetectionConfiguration}. If none is given initially, every invocation to + * {@link #detectCustomImplementation(String, String, ImplementationDetectionConfiguration)} will issue a new component + * scan. * * @author Oliver Gierke * @author Mark Paluch @@ -46,76 +44,91 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Mark Paluch */ -@RequiredArgsConstructor public class CustomRepositoryImplementationDetector { - private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/%s.class"; + private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/*%s.class"; private static final String AMBIGUOUS_CUSTOM_IMPLEMENTATIONS = "Ambiguous custom implementations detected! Found %s but expected a single implementation!"; - private final @NonNull MetadataReaderFactory metadataReaderFactory; - private final @NonNull Environment environment; - private final @NonNull ResourceLoader resourceLoader; + private final Environment environment; + private final ResourceLoader resourceLoader; + private final Lazy> implementationCandidates; /** - * Tries to detect a custom implementation for a repository bean by classpath scanning. - * - * @param configuration the {@link RepositoryConfiguration} to consider. - * @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found. + * Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment}, + * {@link ResourceLoader} and {@link ImplementationDetectionConfiguration}. The latter will be registered for a + * one-time component scan for implementation candidates that will the be used and filtered in all subsequent calls to + * {@link #detectCustomImplementation(RepositoryConfiguration)}. + * + * @param environment must not be {@literal null}. + * @param resourceLoader must not be {@literal null}. + * @param configuration must not be {@literal null}. */ - @SuppressWarnings("deprecation") - public Optional detectCustomImplementation(RepositoryConfiguration configuration) { + public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader, + ImplementationDetectionConfiguration configuration) { - // TODO 2.0: Extract into dedicated interface for custom implementation lookup configuration. + Assert.notNull(environment, "Environment must not be null!"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); + Assert.notNull(configuration, "ImplementationDetectionConfiguration must not be null!"); - return detectCustomImplementation( // - configuration.getImplementationClassName(), // - configuration.getImplementationBeanName(), // - configuration.getImplementationBasePackages(), // - configuration.getExcludeFilters(), // - bd -> configuration.getConfigurationSource().generateBeanName(bd)); + this.environment = environment; + this.resourceLoader = resourceLoader; + this.implementationCandidates = Lazy.of(() -> findCandidateBeanDefinitions(configuration)); + } + + /** + * Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment} and + * {@link ResourceLoader}. Calls to {@link #detectCustomImplementation(ImplementationLookupConfiguration)} will issue + * scans for + * + * @param environment must not be {@literal null}. + * @param resourceLoader must not be {@literal null}. + */ + public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader) { + + Assert.notNull(environment, "Environment must not be null!"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); + + this.environment = environment; + this.resourceLoader = resourceLoader; + this.implementationCandidates = Lazy.empty(); } /** * Tries to detect a custom implementation for a repository bean by classpath scanning. * - * @param className must not be {@literal null}. - * @param beanName may be {@literal null} - * @param basePackages must not be {@literal null}. - * @param excludeFilters must not be {@literal null}. - * @param beanNameGenerator must not be {@literal null}. + * @param lookup must not be {@literal null}. * @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found. */ - public Optional detectCustomImplementation(String className, @Nullable String beanName, - Iterable basePackages, Iterable excludeFilters, - Function beanNameGenerator) { + public Optional detectCustomImplementation(ImplementationLookupConfiguration lookup) { - Assert.notNull(className, "ClassName must not be null!"); - Assert.notNull(basePackages, "BasePackages must not be null!"); + Assert.notNull(lookup, "ImplementationLookupConfiguration must not be null!"); - Set definitions = findCandidateBeanDefinitions(className, basePackages, excludeFilters); + Set definitions = implementationCandidates.getOptional() + .orElseGet(() -> findCandidateBeanDefinitions(lookup)).stream() // + .filter(lookup::matches) // + .collect(StreamUtils.toUnmodifiableSet()); return SelectionSet // .of(definitions, c -> c.isEmpty() ? Optional.empty() : throwAmbiguousCustomImplementationException(c)) // - .filterIfNecessary(bd -> beanName != null && beanName.equals(beanNameGenerator.apply(bd)))// - .uniqueResult().map(it -> AbstractBeanDefinition.class.cast(it)); + .filterIfNecessary(lookup::hasMatchingBeanName) // + .uniqueResult() // + .map(AbstractBeanDefinition.class::cast); } - Set findCandidateBeanDefinitions(String className, Iterable basePackages, - Iterable excludeFilters) { + private Set findCandidateBeanDefinitions(ImplementationDetectionConfiguration config) { - // Build pattern to lookup implementation class + String postfix = config.getImplementationPostfix(); - // Build classpath scanner and lookup bean definition ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, environment); provider.setResourceLoader(resourceLoader); - provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, className)); - provider.setMetadataReaderFactory(metadataReaderFactory); + provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, postfix)); + provider.setMetadataReaderFactory(config.getMetadataReaderFactory()); provider.addIncludeFilter((reader, factory) -> true); - excludeFilters.forEach(it -> provider.addExcludeFilter(it)); + config.getExcludeFilters().forEach(it -> provider.addExcludeFilter(it)); - return Streamable.of(basePackages).stream()// + return config.getBasePackages().stream()// .flatMap(it -> provider.findCandidateComponents(it).stream())// .collect(Collectors.toSet()); } diff --git a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java new file mode 100644 index 000000000..372715328 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java @@ -0,0 +1,170 @@ +/* + * Copyright 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.repository.config; + +import java.io.IOException; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of {@link ImplementationLookupConfiguration}. + * + * @author Oliver Gierke + * @since 2.1 + */ +class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration { + + private final ImplementationDetectionConfiguration config; + private final String interfaceName; + private final String beanName; + + /** + * Creates a new {@link DefaultImplementationLookupConfiguration} for the given + * {@link ImplementationDetectionConfiguration} and interface name. + * + * @param config must not be {@literal null}. + * @param implementationClassName must not be {@literal null} or empty. + */ + DefaultImplementationLookupConfiguration(ImplementationDetectionConfiguration config, String interfaceName) { + + Assert.notNull(config, "ImplementationDetectionConfiguration must not be null!"); + Assert.hasText(interfaceName, "Interface name must not be null or empty!"); + + this.config = config; + this.interfaceName = interfaceName; + this.beanName = StringUtils + .uncapitalize(ClassUtils.getShortName(interfaceName).concat(config.getImplementationPostfix())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationBeanName() + */ + @Override + public String getImplementationBeanName() { + return beanName; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getImplementationPostfix() + */ + @Override + public String getImplementationPostfix() { + return config.getImplementationPostfix(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getExcludeFilters() + */ + @Override + public Streamable getExcludeFilters() { + return config.getExcludeFilters().and(Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getMetadataReaderFactory() + */ + @Override + public MetadataReaderFactory getMetadataReaderFactory() { + return config.getMetadataReaderFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return Streamable.of(ClassUtils.getPackageName(interfaceName)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationClassName() + */ + @Override + public String getImplementationClassName() { + return ClassUtils.getShortName(interfaceName).concat(getImplementationPostfix()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#hasMatchingBeanName(org.springframework.beans.factory.config.BeanDefinition) + */ + @Override + public boolean hasMatchingBeanName(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + return beanName != null && beanName.equals(config.generateBeanName(definition)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#matches(org.springframework.beans.factory.config.BeanDefinition, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean matches(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + String beanClassName = definition.getBeanClassName(); + + if (beanClassName == null || isExcluded(beanClassName, getExcludeFilters())) { + return false; + } + + String beanPackage = ClassUtils.getPackageName(beanClassName); + String shortName = ClassUtils.getShortName(beanClassName); + String localName = shortName.substring(shortName.lastIndexOf('.') + 1); + + return localName.equals(getImplementationClassName()) // + && getBasePackages().stream().anyMatch(it -> beanPackage.startsWith(it)); + } + + private boolean isExcluded(String beanClassName, Streamable filters) { + + try { + + MetadataReader reader = getMetadataReaderFactory().getMetadataReader(beanClassName); + return filters.stream().anyMatch(it -> matches(it, reader)); + + } catch (IOException o_O) { + return true; + } + } + + private boolean matches(TypeFilter filter, MetadataReader reader) { + + try { + return filter.match(reader, getMetadataReaderFactory()); + } catch (IOException e) { + return false; + } + } +} diff --git a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java index e69ca1789..2c9995515 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -21,11 +21,13 @@ import lombok.RequiredArgsConstructor; import java.util.Optional; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.config.ConfigurationUtils; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -179,4 +181,28 @@ public class DefaultRepositoryConfiguration getExcludeFilters() { return configurationSource.getExcludeFilters(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfiguration#toImplementationDetectionConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + + Assert.notNull(factory, "MetadataReaderFactory must not be null!"); + + return configurationSource.toImplementationDetectionConfiguration(factory); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfiguration#toLookupConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { + + Assert.notNull(factory, "MetadataReaderFactory must not be null!"); + + return toImplementationDetectionConfiguration(factory).forRepositoryConfiguration(this); + } } diff --git a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java index a71a48487..f83a48041 100644 --- a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java @@ -15,34 +15,44 @@ */ package org.springframework.data.repository.config; -import lombok.Value; +import lombok.RequiredArgsConstructor; import java.io.IOException; -import java.util.Collections; -import java.util.List; +import java.util.Arrays; import java.util.stream.Stream; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.repository.NoRepositoryBean; -import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Value object for a discovered Repository fragment interface. * * @author Mark Paluch + * @author Oliver Gierke * @since 2.1 */ -@Value(staticConstructor = "of") +@RequiredArgsConstructor public class FragmentMetadata { - private String fragmentInterfaceName; - private RepositoryFragmentDiscovery configuration; + private final MetadataReaderFactory factory; + + /** + * Returns all interfaces to be considered fragment ones for the given source interface. + * + * @param interfaceName must not be {@literal null} or empty. + * @return + */ + public Stream getFragmentInterfaces(String interfaceName) { + + Assert.hasText(interfaceName, "Interface name must not be null or empty!"); + + return Arrays.stream(getClassMetadata(interfaceName).getInterfaceNames()) // + .filter(this::isCandidate); + } /** * Returns whether the given interface is a fragment candidate. @@ -51,55 +61,28 @@ public class FragmentMetadata { * @param factory must not be {@literal null}. * @return */ - public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) { + private boolean isCandidate(String interfaceName) { Assert.hasText(interfaceName, "Interface name must not be null or empty!"); - Assert.notNull(factory, "MetadataReaderFactory must not be null!"); - - AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory); + AnnotationMetadata metadata = getAnnotationMetadata(interfaceName); return !metadata.hasAnnotation(NoRepositoryBean.class.getName()); + } - /** - * Returns the exclusions to be used when scanning for fragment implementations. - * - * @return - */ - public List getExclusions() { - - Stream configurationExcludes = configuration.getExcludeFilters().stream(); - Stream noRepositoryBeans = Stream.of(new AnnotationTypeFilter(NoRepositoryBean.class)); - - return Stream.concat(configurationExcludes, noRepositoryBeans).collect(StreamUtils.toUnmodifiableList()); - } - - /** - * Returns the name of the implementation class to be detected for the fragment interface. - * - * @return - */ - public String getFragmentImplementationClassName() { - - String postfix = configuration.getRepositoryImplementationPostfix().orElse("Impl"); - - return ClassUtils.getShortName(fragmentInterfaceName).concat(postfix); - } - - /** - * Returns the base packages to be scanned to find implementations of the current fragment interface. - * - * @return - */ - public Iterable getBasePackages() { - return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName)); - } - - private static AnnotationMetadata getAnnotationMetadata(String className, - MetadataReaderFactory metadataReaderFactory) { + private AnnotationMetadata getAnnotationMetadata(String className) { try { - return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata(); + return factory.getMetadataReader(className).getAnnotationMetadata(); + } catch (IOException e) { + throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); + } + } + + private ClassMetadata getClassMetadata(String className) { + + try { + return factory.getMetadataReader(className).getClassMetadata(); } catch (IOException e) { throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); } diff --git a/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java new file mode 100644 index 000000000..16aba7c08 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 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.repository.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Expresses configuration to be used to detect implementation classes for repositories and repository fragments. + * + * @author Oliver Gierke + * @since 2.1 + */ +public interface ImplementationDetectionConfiguration { + + /** + * Returns the postfix to be used to calculate the implementation type's name. + * + * @return must not be {@literal null}. + */ + String getImplementationPostfix(); + + /** + * Return the base packages to be scanned for implementation types. + * + * @return must not be {@literal null}. + */ + Streamable getBasePackages(); + + /** + * Returns the exclude filters to be used for the implementation class scanning. + * + * @return must not be {@literal null}. + */ + Streamable getExcludeFilters(); + + /** + * Returns the {@link MetadataReaderFactory} to be used for implementation class scanning. + * + * @return must not be {@literal null}. + */ + MetadataReaderFactory getMetadataReaderFactory(); + + /** + * Generate the bean name for the given {@link BeanDefinition}. + * + * @param definition must not be {@literal null}. + * @return + */ + default String generateBeanName(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + String beanName = definition.getBeanClassName(); + + if (beanName == null) { + throw new IllegalStateException("Cannot generate bean name for BeanDefinition without bean class name!"); + } + + return StringUtils.uncapitalize(ClassUtils.getShortName(beanName)); + } + + /** + * Returns the final lookup configuration for the given fully-qualified fragment interface name. + * + * @param fragmentInterfaceName must not be {@literal null} or empty. + * @return + */ + default ImplementationLookupConfiguration forFragment(String fragmentInterfaceName) { + + Assert.hasText(fragmentInterfaceName, "Fragment interface name must not be null or empty!"); + + return new DefaultImplementationLookupConfiguration(this, fragmentInterfaceName); + } + + /** + * Returns the final lookup configuration for the given {@link RepositoryConfiguration}. + * + * @param config must not be {@literal null}. + * @return + */ + default ImplementationLookupConfiguration forRepositoryConfiguration(RepositoryConfiguration config) { + + Assert.notNull(config, "RepositoryConfiguration must not be null!"); + + return new DefaultImplementationLookupConfiguration(this, config.getRepositoryInterface()) { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.DefaultImplementationLookupConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return config.getImplementationBasePackages(); + } + }; + } +} diff --git a/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java new file mode 100644 index 000000000..82fdb1a3c --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.repository.config; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Configuration that's used to lookup an implementation type for a repository or fragment interface. + * + * @author Oliver Gierke + * @since 2.1 + */ +public interface ImplementationLookupConfiguration extends ImplementationDetectionConfiguration { + + /** + * Returns the bean name of the implementation to be looked up. + * + * @return must not be {@literal null}. + */ + String getImplementationBeanName(); + + /** + * Returns the simple name of the class to be looked up. + * + * @return must not be {@literal null}. + */ + String getImplementationClassName(); + + /** + * Return whether the given {@link BeanDefinition} matches the lookup configuration. + * + * @param definition must not be {@literal null}. + * @return + */ + boolean matches(BeanDefinition definition); + + /** + * Returns whether the bean name created for the given bean definition results in the one required. Will be used to + * disambiguate between multiple {@link BeanDefinition}s matching in general. + * + * @param definition must not be {@literal null}. + * @return + * @see #matches(BeanDefinition) + */ + boolean hasMatchingBeanName(BeanDefinition definition); +} diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index b52b67137..6169afb3d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -15,10 +15,6 @@ */ package org.springframework.data.repository.config; -import lombok.RequiredArgsConstructor; - -import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -26,21 +22,17 @@ import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.config.ParsingUtils; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; import org.springframework.data.util.Optionals; -import org.springframework.data.util.Streamable; import org.springframework.util.Assert; /** @@ -60,6 +52,7 @@ class RepositoryBeanDefinitionBuilder { private final ResourceLoader resourceLoader; private final MetadataReaderFactory metadataReaderFactory; + private final FragmentMetadata fragmentMetadata; private final CustomRepositoryImplementationDetector implementationDetector; /** @@ -72,7 +65,7 @@ class RepositoryBeanDefinitionBuilder { * @param environment must not be {@literal null}. */ public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, - ResourceLoader resourceLoader, Environment environment) { + RepositoryConfigurationSource configurationSource, ResourceLoader resourceLoader, Environment environment) { Assert.notNull(extension, "RepositoryConfigurationExtension must not be null!"); Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); @@ -81,9 +74,12 @@ class RepositoryBeanDefinitionBuilder { this.registry = registry; this.extension = extension; this.resourceLoader = resourceLoader; + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); - this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, - resourceLoader); + + this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory); + this.implementationDetector = new CustomRepositoryImplementationDetector(environment, resourceLoader, + configurationSource.toImplementationDetectionConfiguration(metadataReaderFactory)); } /** @@ -135,22 +131,23 @@ class RepositoryBeanDefinitionBuilder { return builder; } - @SuppressWarnings("deprecation") private Optional registerCustomImplementation(RepositoryConfiguration configuration) { - String beanName = configuration.getImplementationBeanName(); + ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory); + + String beanName = lookup.getImplementationBeanName(); // Already a bean configured? if (registry.containsBeanDefinition(beanName)) { return Optional.of(beanName); } - Optional beanDefinition = implementationDetector.detectCustomImplementation(configuration); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); return beanDefinition.map(it -> { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Registering custom repository implementation: " + configuration.getImplementationBeanName() + " " + LOGGER.debug("Registering custom repository implementation: " + lookup.getImplementationBeanName() + " " + it.getBeanClassName()); } @@ -164,27 +161,23 @@ class RepositoryBeanDefinitionBuilder { private Stream registerRepositoryFragmentsImplementation( RepositoryConfiguration configuration) { - ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface()); - RepositoryFragmentDiscovery fragmentConfiguration = new DefaultRepositoryFragmentDiscovery(configuration); + ImplementationDetectionConfiguration config = configuration + .toImplementationDetectionConfiguration(metadataReaderFactory); - return Arrays.stream(classMetadata.getInterfaceNames()) // - .filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) // - .map(it -> FragmentMetadata.of(it, fragmentConfiguration)) // - .map(it -> detectRepositoryFragmentConfiguration(it, configuration.getConfigurationSource())) // + return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) // + .map(it -> detectRepositoryFragmentConfiguration(it, config)) // .flatMap(Optionals::toStream) // .peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) // .peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)); } - private Optional detectRepositoryFragmentConfiguration( - FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) { + private Optional detectRepositoryFragmentConfiguration(String fragmentInterface, + ImplementationDetectionConfiguration config) { - String className = configuration.getFragmentImplementationClassName(); + ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); - Optional beanDefinition = implementationDetector.detectCustomImplementation(className, null, - configuration.getBasePackages(), configuration.getExclusions(), configurationSource::generateBeanName); - - return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); + return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd)); } private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration repositoryConfiguration, @@ -232,37 +225,4 @@ class RepositoryBeanDefinitionBuilder { registry.registerBeanDefinition(beanName, ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource())); } - - private ClassMetadata getClassMetadata(String className) { - - try { - return metadataReaderFactory.getMetadataReader(className).getClassMetadata(); - } catch (IOException e) { - throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); - } - } - - @RequiredArgsConstructor - private static class DefaultRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery { - - private final RepositoryConfiguration configuration; - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters() - */ - @Override - public Streamable getExcludeFilters() { - return configuration.getExcludeFilters(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix() - */ - @Override - public Optional getRepositoryImplementationPostfix() { - return configuration.getConfigurationSource().getRepositoryImplementationPostfix(); - } - } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java index 662e136bb..c97e42f31 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.data.repository.config; import java.util.Optional; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.util.Streamable; @@ -67,24 +68,6 @@ public interface RepositoryConfiguration getNamedQueriesLocation(); - /** - * Returns the class name of the custom implementation. - * - * @return - * @deprecated since 2.0. Use repository compositions by creating mixins. - */ - @Deprecated - String getImplementationClassName(); - - /** - * Returns the bean name of the custom implementation. - * - * @return - * @deprecated since 2.0. Use repository compositions by creating mixins. - */ - @Deprecated - String getImplementationBeanName(); - /** * Returns the name of the repository base class to be used or {@literal null} if the store specific defaults shall be * applied. @@ -129,4 +112,22 @@ public interface RepositoryConfiguration getExcludeFilters(); + + /** + * Returns the {@link ImplementationDetectionConfiguration} to be used for this repository. + * + * @param factory must not be {@literal null}. + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory); + + /** + * Returns the {@link ImplementationLookupConfiguration} for the given {@link MetadataReaderFactory}. + * + * @param factory must not be {@literal null}. + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index 9688ac496..0eec3260b 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -113,8 +113,8 @@ public class RepositoryConfigurationDelegate { extension.registerBeansForRoot(registry, configurationSource); - RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, resourceLoader, - environment); + RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, + configurationSource, resourceLoader, environment); List definitions = new ArrayList<>(); if (LOG.isDebugEnabled()) { diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java index a5c5cea99..9b5ef4206 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.util.Streamable; @@ -126,4 +127,14 @@ public interface RepositoryConfigurationSource { * @since 2.0 */ String generateBeanName(BeanDefinition beanDefinition); + + /** + * Returns the {@link ImplementationDetectionConfiguration} to be used to scan for custom implementations of the + * repository instances to be created from this {@link RepositoryConfigurationSource}. + * + * @param factory + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java index 8154ac649..3da34afec 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java @@ -15,12 +15,16 @@ */ package org.springframework.data.repository.config; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + import java.util.Collections; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -117,4 +121,57 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository public boolean shouldConsiderNestedRepositories() { return false; } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationSource#toImplementationDetectionConfiguration() + */ + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + return new SpringImplementationDetectionConfiguration(this, factory); + } + + @RequiredArgsConstructor + private class SpringImplementationDetectionConfiguration implements ImplementationDetectionConfiguration { + + private final RepositoryConfigurationSource source; + private final @Getter MetadataReaderFactory metadataReaderFactory; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix() + */ + @Override + public String getImplementationPostfix() { + return source.getRepositoryImplementationPostfix() + .orElse(DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return source.getBasePackages(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters() + */ + @Override + public Streamable getExcludeFilters() { + return source.getExcludeFilters(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#generateBeanName(org.springframework.beans.factory.config.BeanDefinition) + */ + @Override + public String generateBeanName(BeanDefinition definition) { + return source.generateBeanName(definition); + } + } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java deleted file mode 100644 index d24988598..000000000 --- a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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.repository.config; - -import java.util.Optional; - -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.data.util.Streamable; - -/** - * Interface containing the configurable options for the Spring Data repository fragment subsystem. - * - * @author Mark Paluch - * @since 2.1 - * @see RepositoryConfigurationSource - */ -public interface RepositoryFragmentDiscovery { - - /** - * Returns the {@link TypeFilter}s to be used to exclude packages from repository scanning. - * - * @return - */ - Streamable getExcludeFilters(); - - /** - * Returns the configured postfix to be used for looking up custom implementation classes. - * - * @return the postfix to use or {@link Optional#empty()} in case none is configured. - */ - Optional getRepositoryImplementationPostfix(); -} diff --git a/src/main/java/org/springframework/data/util/Lazy.java b/src/main/java/org/springframework/data/util/Lazy.java index 840c2d285..4b2d36be2 100644 --- a/src/main/java/org/springframework/data/util/Lazy.java +++ b/src/main/java/org/springframework/data/util/Lazy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.util; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -35,9 +37,12 @@ import org.springframework.util.Assert; * @since 2.0 */ @RequiredArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode public class Lazy implements Supplier { + private static final Lazy EMPTY = new Lazy<>(() -> null, null, true); + private final Supplier supplier; private @Nullable T value = null; private boolean resolved = false; @@ -67,6 +72,17 @@ public class Lazy implements Supplier { return new Lazy<>(() -> value); } + /** + * Creates a pre-resolved empty {@link Lazy}. + * + * @return + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public static Lazy empty() { + return (Lazy) EMPTY; + } + /** * Returns the value created by the configured {@link Supplier}. Will return the calculated instance for subsequent * lookups. diff --git a/src/main/java/org/springframework/data/util/Streamable.java b/src/main/java/org/springframework/data/util/Streamable.java index a3a0802e4..d489083b6 100644 --- a/src/main/java/org/springframework/data/util/Streamable.java +++ b/src/main/java/org/springframework/data/util/Streamable.java @@ -30,9 +30,10 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Christoph Strobl + * @since 2.0 */ @FunctionalInterface -public interface Streamable extends Iterable { +public interface Streamable extends Iterable, Supplier> { /** * Returns an empty {@link Streamable}. @@ -130,4 +131,26 @@ public interface Streamable extends Iterable { default boolean isEmpty() { return !iterator().hasNext(); } + + /** + * Creates a new {@link Streamable} from the current one and the given {@link Stream} concatenated. + * + * @param stream must not be {@literal null}. + * @return + * @since 2.1 + */ + default Streamable and(Supplier> stream) { + + Assert.notNull(stream, "Stream must not be null!"); + + return Streamable.of(() -> Stream.concat(this.stream(), stream.get())); + } + + /* + * (non-Javadoc) + * @see java.util.function.Supplier#get() + */ + default Stream get() { + return stream(); + } } diff --git a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java index 69c584cb4..e42a346fb 100755 --- a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java @@ -16,7 +16,6 @@ package org.springframework.data.repository.cdi; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.io.Serializable; @@ -25,7 +24,6 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.spi.CreationalContext; @@ -35,11 +33,12 @@ import javax.inject.Named; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; +import org.springframework.data.repository.config.ImplementationLookupConfiguration; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -153,13 +152,15 @@ public class CdiRepositoryBeanUnitTests { bean.create(mock(CreationalContext.class), SampleRepository.class); - verify(detector).detectCustomImplementation( // - eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), // - eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), // - anySet(), // - anySet(), // - Mockito.any(Function.class) // - ); + ArgumentCaptor captor = ArgumentCaptor + .forClass(ImplementationLookupConfiguration.class); + + verify(detector).detectCustomImplementation(captor.capture()); + + ImplementationLookupConfiguration configuration = captor.getValue(); + + assertThat(configuration.getImplementationBeanName()).isEqualTo("cdiRepositoryBeanUnitTests.SampleRepositoryImpl"); + assertThat(configuration.getImplementationClassName()).isEqualTo("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"); } @Test // DATACMNS-1233 diff --git a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java index 89641d89b..675673cd1 100644 --- a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java @@ -15,18 +15,14 @@ */ package org.springframework.data.repository.config; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.HashSet; import java.util.Optional; -import java.util.function.Function; -import org.assertj.core.api.Assertions; import org.junit.Test; +import org.mockito.Answers; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.core.env.Environment; @@ -34,7 +30,8 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; +import org.springframework.data.repository.config.CustomRepositoryImplementationDetectorUnitTests.First.CanonicalSampleRepositoryTestImpl; +import org.springframework.data.util.Streamable; import org.springframework.mock.env.MockEnvironment; /** @@ -47,70 +44,107 @@ public class CustomRepositoryImplementationDetectorUnitTests { MetadataReaderFactory metadataFactory = new SimpleMetadataReaderFactory(); Environment environment = new MockEnvironment(); ResourceLoader resourceLoader = new DefaultResourceLoader(); - Function nameGenerator = mock(Function.class); + ImplementationDetectionConfiguration configuration = mock(ImplementationDetectionConfiguration.class, + Answers.RETURNS_MOCKS); - CustomRepositoryImplementationDetector detector = spy( - new CustomRepositoryImplementationDetector(metadataFactory, environment, resourceLoader)); + CustomRepositoryImplementationDetector detector = new CustomRepositoryImplementationDetector(environment, + resourceLoader, configuration); { - doReturn("notTheBeanYouAreLookingFor").when(nameGenerator).apply(any(BeanDefinition.class)); + when(configuration.forRepositoryConfiguration(any(RepositoryConfiguration.class))).thenCallRealMethod(); + when(configuration.getMetadataReaderFactory()).thenReturn(metadataFactory); + when(configuration.getBasePackages()).thenReturn(Streamable.of(this.getClass().getPackage().getName())); + when(configuration.getImplementationPostfix()).thenReturn("TestImpl"); } - @Test // DATACMNS-764 + @Test // DATACMNS-764, DATACMNS-1371 public void returnsNullWhenNoImplementationFound() { - doReturn(emptySet()).when(detector).findCandidateBeanDefinitions(anyString(), anyListOf(String.class), - anyListOf(TypeFilter.class)); + RepositoryConfiguration mock = mock(RepositoryConfiguration.class); - Optional beanDefinition = detector.detectCustomImplementation("className", "beanName", emptyList(), - emptyList(), nameGenerator); + ImplementationLookupConfiguration lookup = configuration + .forRepositoryConfiguration(configFor(NoImplementationRepository.class)); + + Optional beanDefinition = detector.detectCustomImplementation(lookup); assertThat(beanDefinition).isEmpty(); } - @Test // DATACMNS-764 + @Test // DATACMNS-764, DATACMNS-1371 public void returnsBeanDefinitionWhenOneImplementationIsFound() { - AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class); + ImplementationLookupConfiguration lookup = configuration + .forRepositoryConfiguration(configFor(SingleSampleRepository.class)); - doReturn(new HashSet<>(singleton(expectedBeanDefinition))).when(detector).findCandidateBeanDefinitions(anyString(), - anyListOf(String.class), anyListOf(TypeFilter.class)); + Optional beanDefinition = detector.detectCustomImplementation(lookup); - Optional beanDefinition = detector.detectCustomImplementation("className", "beanName", emptyList(), - emptyList(), nameGenerator); - - assertThat(beanDefinition).contains(expectedBeanDefinition); + assertThat(beanDefinition).hasValueSatisfying( + it -> assertThat(it.getBeanClassName()).isEqualTo(SingleSampleRepositoryTestImpl.class.getName())); } - @Test // DATACMNS-764 + @Test // DATACMNS-764, DATACMNS-1371 public void returnsBeanDefinitionMatchingByNameWhenMultipleImplementationAreFound() { - AbstractBeanDefinition wrongBeanDefinition = mock(AbstractBeanDefinition.class); - AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class); + when(configuration.generateBeanName(any())).then(it -> { - doReturn("expected").when(nameGenerator).apply(expectedBeanDefinition); + BeanDefinition definition = it.getArgument(0); + String className = definition.getBeanClassName(); - doReturn(new HashSet<>(asList(wrongBeanDefinition, expectedBeanDefinition))).when(detector) - .findCandidateBeanDefinitions(anyString(), anyListOf(String.class), anyListOf(TypeFilter.class)); + return className.contains("$First$") ? "canonicalSampleRepositoryTestImpl" : "otherBeanName"; + }); - Optional beanDefinition = detector.detectCustomImplementation("className", "expected", emptyList(), - emptyList(), nameGenerator); + ImplementationLookupConfiguration lookup = configuration + .forRepositoryConfiguration(configFor(CanonicalSampleRepository.class)); - assertThat( beanDefinition).contains(expectedBeanDefinition); + assertThat(detector.detectCustomImplementation(lookup)) // + .hasValueSatisfying( + it -> assertThat(it.getBeanClassName()).isEqualTo(CanonicalSampleRepositoryTestImpl.class.getName())); } - @Test(expected = IllegalStateException.class) // DATACMNS-764 + @Test // DATACMNS-764, DATACMNS-1371 public void throwsExceptionWhenMultipleImplementationAreFound() { - AbstractBeanDefinition wrongBeanDefinition = mock(AbstractBeanDefinition.class); - AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> { - doReturn("expected").when(nameGenerator).apply(any(BeanDefinition.class)); + ImplementationLookupConfiguration lookup = mock(ImplementationLookupConfiguration.class); - doReturn(new HashSet<>(asList(wrongBeanDefinition, expectedBeanDefinition))).when(detector) - .findCandidateBeanDefinitions(anyString(), anyListOf(String.class), anyListOf(TypeFilter.class)); + when(lookup.hasMatchingBeanName(any())).thenReturn(true); + when(lookup.matches(any())).thenReturn(true); - Optional beanDefinition = detector.detectCustomImplementation("className", "expected", emptyList(), - emptyList(), nameGenerator); + detector.detectCustomImplementation(lookup); + }); + } + + private RepositoryConfiguration configFor(Class type) { + + RepositoryConfiguration configuration = mock(RepositoryConfiguration.class); + + when(configuration.getRepositoryInterface()).thenReturn(type.getSimpleName()); + when(configuration.getImplementationBasePackages()) + .thenReturn(Streamable.of(this.getClass().getPackage().getName())); + + return configuration; + } + + // No implementation + + interface NoImplementationRepository {} + + // Single implementation + + interface SingleSampleRepository {} + + static class SingleSampleRepositoryTestImpl implements SingleSampleRepository {} + + // Multiple implementations + + interface CanonicalSampleRepository {} + + static class First { + static class CanonicalSampleRepositoryTestImpl implements CanonicalSampleRepository {} + } + + static class Second { + static class CanonicalSampleRepositoryTestImpl implements CanonicalSampleRepository {} } } diff --git a/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java index e67f15720..0de15c311 100755 --- a/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java @@ -23,14 +23,10 @@ import lombok.Value; import java.util.Optional; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.data.repository.query.QueryLookupStrategy.Key; @@ -49,22 +45,12 @@ public class DefaultRepositoryConfigurationUnitTests { RepositoryConfigurationExtension extension = new SimplerRepositoryConfigurationExtension("factory", "module"); - @Before - public void before() { - - RepositoryBeanNameGenerator generator = new RepositoryBeanNameGenerator(getClass().getClassLoader()); - Answer answer = invocation -> generator.generateBeanName((BeanDefinition) invocation.getArguments()[0]); - when(source.generateBeanName(Mockito.any(BeanDefinition.class))).then(answer); - } - @Test public void supportsBasicConfiguration() { RepositoryConfiguration configuration = getConfiguration(source); assertThat(configuration.getConfigurationSource()).isEqualTo(source); - assertThat(configuration.getImplementationBeanName()).isEqualTo("myRepositoryImpl"); - assertThat(configuration.getImplementationClassName()).isEqualTo("MyRepositoryImpl"); assertThat(configuration.getRepositoryInterface()).isEqualTo("com.acme.MyRepository"); assertThat(configuration.getQueryLookupStrategyKey()).isEqualTo(Key.CREATE_IF_NOT_FOUND); assertThat(configuration.isLazyInit()).isFalse();