diff --git a/pom.xml b/pom.xml index 6ca40eba5c..cf5c529ce8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATACMNS-1255-SNAPSHOT Spring Data Core diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java index 9342d144b8..109fbc2b7e 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java @@ -25,12 +25,12 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Stereotype; -import javax.enterprise.inject.UnsatisfiedResolutionException; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.InjectionPoint; @@ -38,15 +38,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; -import org.springframework.data.repository.config.DefaultRepositoryConfiguration; -import org.springframework.data.repository.config.RepositoryBeanNameGenerator; -import org.springframework.data.repository.config.SpringDataAnnotationBeanNameGenerator; +import org.springframework.data.repository.config.RepositoryFragmentConfiguration; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -66,16 +64,12 @@ public abstract class CdiRepositoryBean implements Bean, PassivationCapabl private final Set qualifiers; private final Class repositoryType; - private final Optional detector; + private final CdiRepositoryContext context; private final BeanManager beanManager; private final String passivationId; private transient @Nullable T repoInstance; - private final SpringDataAnnotationBeanNameGenerator annotationBeanNameGenerator = new SpringDataAnnotationBeanNameGenerator(); - private final RepositoryBeanNameGenerator beanNameGenerator = new RepositoryBeanNameGenerator( - getClass().getClassLoader()); - /** * Creates a new {@link CdiRepositoryBean}. * @@ -84,7 +78,7 @@ public abstract class CdiRepositoryBean implements Bean, PassivationCapabl * @param beanManager the CDI {@link BeanManager}, must not be {@literal null}. */ public CdiRepositoryBean(Set qualifiers, Class repositoryType, BeanManager beanManager) { - this(qualifiers, repositoryType, beanManager, Optional.empty()); + this(qualifiers, repositoryType, beanManager, new CdiRepositoryContext(CdiRepositoryBean.class.getClassLoader())); } /** @@ -93,8 +87,7 @@ public CdiRepositoryBean(Set qualifiers, Class repositoryType, Be * @param qualifiers must not be {@literal null}. * @param repositoryType has to be an interface must not be {@literal null}. * @param beanManager the CDI {@link BeanManager}, must not be {@literal null}. - * @param detector detector for the custom repository implementations {@link CustomRepositoryImplementationDetector}, - * can be {@literal null}. + * @param detector detector for the custom repository implementations {@link CustomRepositoryImplementationDetector}. */ public CdiRepositoryBean(Set qualifiers, Class repositoryType, BeanManager beanManager, Optional detector) { @@ -107,7 +100,32 @@ public CdiRepositoryBean(Set qualifiers, Class repositoryType, Be this.qualifiers = qualifiers; this.repositoryType = repositoryType; this.beanManager = beanManager; - this.detector = detector; + this.context = new CdiRepositoryContext(getClass().getClassLoader(), detector + .orElseThrow(() -> new IllegalArgumentException("CustomRepositoryImplementationDetector must be present!"))); + this.passivationId = createPassivationId(qualifiers, repositoryType); + } + + /** + * Creates a new {@link CdiRepositoryBean}. + * + * @param qualifiers must not be {@literal null}. + * @param repositoryType has to be an interface must not be {@literal null}. + * @param beanManager the CDI {@link BeanManager}, must not be {@literal null}. + * @param context CDI context encapsulating class loader, metadata scanning and fragment detection. + * @since 2.1 + */ + public CdiRepositoryBean(Set qualifiers, Class repositoryType, BeanManager beanManager, + CdiRepositoryContext context) { + + Assert.notNull(qualifiers, "Qualifiers must not be null!"); + Assert.notNull(beanManager, "BeanManager must not be null!"); + Assert.notNull(repositoryType, "Repoitory type must not be null!"); + Assert.isTrue(repositoryType.isInterface(), "RepositoryType must be an interface!"); + + this.qualifiers = qualifiers; + this.repositoryType = repositoryType; + this.beanManager = beanManager; + this.context = context; this.passivationId = createPassivationId(qualifiers, repositoryType); } @@ -128,7 +146,6 @@ private String createPassivationId(Set qualifiers, Class reposito Collections.sort(qualifierNames); return StringUtils.collectionToDelimitedString(qualifierNames, ":") + ":" + repositoryType.getName(); - } /* @@ -215,89 +232,6 @@ public void destroy(@SuppressWarnings("null") T instance, creationalContext.release(); } - /** - * Looks up an instance of a {@link CdiRepositoryConfiguration}. In case the instance cannot be found within the CDI - * scope, a default configuration is used. - * - * @return an available CdiRepositoryConfiguration instance or a default configuration. - */ - protected CdiRepositoryConfiguration lookupConfiguration(BeanManager beanManager, Set qualifiers) { - - return beanManager.getBeans(CdiRepositoryConfiguration.class, getQualifiersArray(qualifiers)).stream().findFirst()// - .map(it -> (CdiRepositoryConfiguration) getDependencyInstance(it)) // - .orElse(DEFAULT_CONFIGURATION); - } - - /** - * Try to lookup a custom implementation for a {@link org.springframework.data.repository.Repository}. Can only be - * used when a {@code CustomRepositoryImplementationDetector} is provided. - * - * @param repositoryType - * @param beanManager - * @param qualifiers - * @return the custom implementation instance or null - */ - private Optional> getCustomImplementationBean(Class repositoryType, BeanManager beanManager, - Set qualifiers) { - - return detector.flatMap(it -> { - - CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers); - - return getCustomImplementationClass(repositoryType, cdiRepositoryConfiguration, it)// - .flatMap(type -> beanManager.getBeans(type, getQualifiersArray(qualifiers)).stream().findFirst()); - }); - } - - /** - * Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can - * find also a custom repository which is inherited over many levels. - * - * @param repositoryType The class representing the repository. - * @param cdiRepositoryConfiguration The configuration for CDI usage. - * @return the interface class or {@literal null}. - */ - private Optional> getCustomImplementationClass(Class repositoryType, - CdiRepositoryConfiguration cdiRepositoryConfiguration, CustomRepositoryImplementationDetector detector) { - - String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration); - Optional beanDefinition = detector.detectCustomImplementation( // - className, // - getCustomImplementationBeanName(repositoryType), // - Collections.singleton(repositoryType.getPackage().getName()), // - Collections.emptySet(), // - beanNameGenerator::generateBeanName // - ); - - return beanDefinition.map(it -> { - - try { - return Class.forName(it.getBeanClassName()); - } catch (ClassNotFoundException e) { - throw new UnsatisfiedResolutionException( - String.format("Unable to resolve class for '%s'", it.getBeanClassName()), e); - } - }); - } - - private String getCustomImplementationBeanName(Class repositoryType) { - return annotationBeanNameGenerator.generateBeanName(new AnnotatedGenericBeanDefinition(repositoryType)) - + DEFAULT_CONFIGURATION.getRepositoryImplementationPostfix(); - } - - private 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; - } - - private Annotation[] getQualifiersArray(Set qualifiers) { - return qualifiers.toArray(new Annotation[qualifiers.size()]); - } - /* * (non-Javadoc) * @see javax.enterprise.inject.spi.Bean#getQualifiers() @@ -380,18 +314,75 @@ public String getId() { * @param creationalContext will never be {@literal null}. * @param repositoryType will never be {@literal null}. * @return - * @deprecated override {@link #create(CreationalContext, Class, Object)} instead. */ - @Deprecated protected T create(CreationalContext creationalContext, Class repositoryType) { - Optional> customImplementationBean = getCustomImplementationBean(repositoryType, beanManager, qualifiers); - Optional customImplementation = customImplementationBean - .map(it -> beanManager.getReference(it, it.getBeanClass(), beanManager.createCreationalContext(it))); + CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers); + + Optional> customImplementationBean = getCustomImplementationBean(repositoryType, + cdiRepositoryConfiguration); + Optional customImplementation = customImplementationBean.map(this::getDependencyInstance); return create(creationalContext, repositoryType, customImplementation); } + /** + * Lookup repository fragments for a {@link Class repository interface}. + * + * @param repositoryType must not be {@literal null}. + * @return the {@link RepositoryFragments}. + * @since 2.1 + */ + protected RepositoryFragments getRepositoryFragments(Class repositoryType) { + + Assert.notNull(repositoryType, "Repository type must not be null!"); + + CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers); + + Optional> customImplementationBean = getCustomImplementationBean(repositoryType, + cdiRepositoryConfiguration); + Optional customImplementation = customImplementationBean.map(this::getDependencyInstance); + + List> repositoryFragments = getRepositoryFragments(repositoryType, + cdiRepositoryConfiguration); + + RepositoryFragments customImplementationFragment = customImplementation // + .map(RepositoryFragments::just) // + .orElseGet(RepositoryFragments::empty); + + return RepositoryFragments.from(repositoryFragments) // + .append(customImplementationFragment); + } + + @SuppressWarnings("unchecked") + private List> getRepositoryFragments(Class repositoryType, + CdiRepositoryConfiguration cdiRepositoryConfiguration) { + + Stream fragmentConfigurations = context + .getRepositoryFragments(cdiRepositoryConfiguration, repositoryType); + + return fragmentConfigurations.flatMap(it -> { + + Class interfaceClass = (Class) lookupFragmentInterface(repositoryType, it.getInterfaceName()); + Class implementationClass = context.loadClass(it.getClassName()); + Optional> bean = getBean(implementationClass, beanManager, qualifiers); + + return bean.map(this::getDependencyInstance) // + .map(implementation -> RepositoryFragment.implemented(interfaceClass, implementation)) // + .map(Stream::of) // + .orElse(Stream.empty()); + }).collect(Collectors.toList()); + } + + private static Class lookupFragmentInterface(Class repositoryType, String interfaceName) { + + return Arrays.stream(repositoryType.getInterfaces()) // + .filter(it -> it.getName().equals(interfaceName)) // + .findFirst() // + .orElseThrow(() -> new IllegalArgumentException(String.format("Did not find type %s in %s!", interfaceName, + Arrays.asList(repositoryType.getInterfaces())))); + } + /** * Creates the actual component instance. * @@ -399,7 +390,10 @@ protected T create(CreationalContext creationalContext, Class repositoryTy * @param repositoryType will never be {@literal null}. * @param customImplementation can be {@literal null}. * @return + * @deprecated since 2.1, override {@link #create(CreationalContext, Class)} in which you create a repository factory + * and call {@link #create(RepositoryFactorySupport, Class, RepositoryFragments)}. */ + @Deprecated protected T create(CreationalContext creationalContext, Class repositoryType, Optional customImplementation) { throw new UnsupportedOperationException( @@ -407,6 +401,83 @@ protected T create(CreationalContext creationalContext, Class repositoryTy + "in order to use custom repository implementations"); } + /** + * Looks up an instance of a {@link CdiRepositoryConfiguration}. In case the instance cannot be found within the CDI + * scope, a default configuration is used. + * + * @return an available CdiRepositoryConfiguration instance or a default configuration. + */ + protected CdiRepositoryConfiguration lookupConfiguration(BeanManager beanManager, Set qualifiers) { + + return beanManager.getBeans(CdiRepositoryConfiguration.class, getQualifiersArray(qualifiers)).stream().findFirst()// + .map(it -> (CdiRepositoryConfiguration) getDependencyInstance(it)) // + .orElse(DEFAULT_CONFIGURATION); + } + + /** + * Try to lookup a custom implementation for a {@link org.springframework.data.repository.Repository}. Can only be + * used when a {@code CustomRepositoryImplementationDetector} is provided. + * + * @param repositoryType + * @param beanManager + * @param qualifiers + * @return the custom implementation instance or null + */ + private Optional> getCustomImplementationBean(Class repositoryType, + CdiRepositoryConfiguration cdiRepositoryConfiguration) { + + return context.getCustomImplementationClass(repositoryType, cdiRepositoryConfiguration)// + .flatMap(type -> getBean(type, beanManager, qualifiers)); + } + + /** + * Applies the configuration from {@link CdiRepositoryConfiguration} to {@link RepositoryFactorySupport} by looking up + * the actual configuration. + * + * @param repositoryFactory will never be {@literal null}. + * @since 2.1 + */ + protected void applyConfiguration(RepositoryFactorySupport repositoryFactory) { + applyConfiguration(repositoryFactory, lookupConfiguration(beanManager, qualifiers)); + } + + /** + * Applies the configuration from {@link CdiRepositoryConfiguration} to {@link RepositoryFactorySupport} by looking up + * the actual configuration. + * + * @param repositoryFactory will never be {@literal null}. + * @param configuration will never be {@literal null}. + * @since 2.1 + */ + protected static void applyConfiguration(RepositoryFactorySupport repositoryFactory, + CdiRepositoryConfiguration configuration) { + + configuration.getEvaluationContextProvider().ifPresent(repositoryFactory::setEvaluationContextProvider); + configuration.getNamedQueries().ifPresent(repositoryFactory::setNamedQueries); + configuration.getQueryLookupStrategy().ifPresent(repositoryFactory::setQueryLookupStrategyKey); + configuration.getRepositoryBeanClass().ifPresent(repositoryFactory::setRepositoryBaseClass); + } + + /** + * Creates the actual repository instance. + * + * @param repositoryType will never be {@literal null}. + * @param repositoryFragments will never be {@literal null}. + * @return + */ + protected static T create(RepositoryFactorySupport repositoryFactory, Class repositoryType, + RepositoryFragments repositoryFragments) { + return repositoryFactory.getRepository(repositoryType, repositoryFragments); + } + + private static Optional> getBean(Class beanType, BeanManager beanManager, Set qualifiers) { + return beanManager.getBeans(beanType, getQualifiersArray(qualifiers)).stream().findFirst(); + } + + private static Annotation[] getQualifiersArray(Set qualifiers) { + return qualifiers.toArray(new Annotation[qualifiers.size()]); + } + /* * (non-Javadoc) * @see java.lang.Object#toString() @@ -417,17 +488,7 @@ public String toString() { qualifiers.toString()); } - static enum DefaultCdiRepositoryConfiguration implements CdiRepositoryConfiguration { - + enum DefaultCdiRepositoryConfiguration implements CdiRepositoryConfiguration { INSTANCE; - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.cdi.CdiRepositoryConfiguration#getRepositoryImplementationPostfix() - */ - @Override - public String getRepositoryImplementationPostfix() { - return DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX; - } } } diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java index 07d913f236..62360ddfc5 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryConfiguration.java @@ -16,17 +16,67 @@ package org.springframework.data.repository.cdi; +import java.util.Optional; + +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryLookupStrategy; + /** * Interface containing the configurable options for the Spring Data repository subsystem using CDI. * * @author Mark Paluch + * @author Fabian Henniges */ public interface CdiRepositoryConfiguration { + /** + * Return the {@link EvaluationContextProvider} to use. Can be {@link Optional#empty()} . + * + * @return the optional {@link EvaluationContextProvider} base to use, can be {@link Optional#empty()}, must not be + * {@literal null}. + * @since 2.1 + */ + default Optional getEvaluationContextProvider() { + return Optional.empty(); + } + + /** + * Return the {@link NamedQueries} to use. Can be {@link Optional#empty()}. + * + * @return the optional named queries to use, can be {@link Optional#empty()}, must not be {@literal null}. + * @since 2.1 + */ + default Optional getNamedQueries() { + return Optional.empty(); + } + + /** + * Return the {@link QueryLookupStrategy.Key} to lookup queries. Can be {@link Optional#empty()}. + * + * @return the lookup strategy to use, can be {@link Optional#empty()}, must not be {@literal null}. + * @since 2.1 + */ + default Optional getQueryLookupStrategy() { + return Optional.empty(); + } + + /** + * Return the {@link Class repository base class} to use. Can be {@link Optional#empty()} . + * + * @return the optional repository base to use, can be {@link Optional#empty()}, must not be {@literal null}. + * @since 2.1 + */ + default Optional> getRepositoryBeanClass() { + return Optional.empty(); + } + /** * Returns the configured postfix to be used for looking up custom implementation classes. * * @return the postfix to use, must not be {@literal null}. */ - String getRepositoryImplementationPostfix(); + default String getRepositoryImplementationPostfix() { + return "Impl"; + } } diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java new file mode 100644 index 0000000000..1c8b95d8e2 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java @@ -0,0 +1,217 @@ +/* + * 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.cdi; + +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.RepositoryFragmentConfiguration; +import org.springframework.data.repository.config.RepositoryFragmentDiscovery; +import org.springframework.data.util.Optionals; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Context for CDI repositories. This class provides {@link ClassLoader} and + * {@link org.springframework.data.repository.core.support.RepositoryFragment detection} which are commonly used within + * CDI. + * + * @author Mark Paluch + * @since 2.1 + */ +public class CdiRepositoryContext { + + private final ClassLoader classLoader; + private final CustomRepositoryImplementationDetector detector; + private final MetadataReaderFactory metadataReaderFactory; + + /** + * Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize + * {@link CachingMetadataReaderFactory}. + * + * @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); + } + + /** + * Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and + * {@link CustomRepositoryImplementationDetector}. + * + * @param classLoader must not be {@literal null}. + * @param detector must not be {@literal null}. + */ + public CdiRepositoryContext(ClassLoader classLoader, CustomRepositoryImplementationDetector detector) { + + Assert.notNull(classLoader, "ClassLoader must not be null!"); + Assert.notNull(detector, "CustomRepositoryImplementationDetector must not be null!"); + + ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader); + + this.classLoader = classLoader; + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.detector = detector; + } + + CustomRepositoryImplementationDetector getCustomRepositoryImplementationDetector() { + return detector; + } + + /** + * Load a {@link Class} using the CDI {@link ClassLoader}. + * + * @param className + * @return + * @throws UnsatisfiedResolutionException if the class cannot be found. + */ + Class loadClass(String className) { + + try { + return ClassUtils.forName(className, classLoader); + } catch (ClassNotFoundException e) { + throw new UnsatisfiedResolutionException(String.format("Unable to resolve class for '%s'", className), e); + } + } + + /** + * Discover {@link RepositoryFragmentConfiguration fragment configurations} for a {@link Class repository interface}. + * + * @param configuration must not be {@literal null}. + * @param repositoryInterface must not be {@literal null}. + * @return {@link Stream} of {@link RepositoryFragmentConfiguration fragment configurations}. + */ + Stream getRepositoryFragments(CdiRepositoryConfiguration configuration, + Class repositoryInterface) { + + ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName()); + + 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) // + .flatMap(Optionals::toStream); + } + + /** + * Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can + * find also a custom repository which is inherited over many levels. + * + * @param repositoryType The class representing the repository. + * @param cdiRepositoryConfiguration The configuration for CDI usage. + * @return the interface class or {@literal null}. + */ + Optional> getCustomImplementationClass(Class repositoryType, + CdiRepositoryConfiguration cdiRepositoryConfiguration) { + + String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration); + + Optional beanDefinition = detector.detectCustomImplementation( // + className, // + className, Collections.singleton(repositoryType.getPackage().getName()), // + Collections.emptySet(), // + BeanDefinition::getBeanClassName); + + return beanDefinition.map(it -> loadClass(it.getBeanClassName())); + } + + private Optional detectRepositoryFragmentConfiguration( + FragmentMetadata configuration) { + + String className = configuration.getFragmentImplementationClassName(); + + Optional beanDefinition = detector.detectCustomImplementation(className, null, + configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName); + + return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); + } + + 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 final CdiRepositoryConfiguration configuration; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#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()); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryExtensionSupport.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryExtensionSupport.java index 7b0c36ab81..5ba1a0d1c5 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryExtensionSupport.java @@ -36,12 +36,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; -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.classreading.CachingMetadataReaderFactory; -import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.Repository; import org.springframework.data.repository.RepositoryDefinition; @@ -61,16 +55,10 @@ public abstract class CdiRepositoryExtensionSupport implements Extension { private final Map, Set> repositoryTypes = new HashMap<>(); private final Set> eagerRepositories = new HashSet<>(); - private final CustomRepositoryImplementationDetector customImplementationDetector; + private final CdiRepositoryContext context; protected CdiRepositoryExtensionSupport() { - - Environment environment = new StandardEnvironment(); - ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); - MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); - - this.customImplementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, - resourceLoader); + context = new CdiRepositoryContext(getClass().getClassLoader()); } /** @@ -91,8 +79,8 @@ protected void processAnnotatedType(@Observes ProcessAnnotatedType proces Set qualifiers = getQualifiers(repositoryType); if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(), - qualifiers)); + LOGGER.debug( + String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(), qualifiers)); } // Store the repository type using its qualifiers. repositoryTypes.put(repositoryType, qualifiers); @@ -184,7 +172,15 @@ protected void registerBean(CdiRepositoryBean bean) { * @return the {@link CustomRepositoryImplementationDetector} to scan for the custom implementation */ protected CustomRepositoryImplementationDetector getCustomImplementationDetector() { - return customImplementationDetector; + return context.getCustomRepositoryImplementationDetector(); + } + + /** + * @return the {@link CdiRepositoryContext} encapsulating the CDI-specific class loaders and fragment scanning. + * @since 2.1 + */ + protected CdiRepositoryContext getRepositoryContext() { + return context; } @SuppressWarnings("all") 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 aca90c3694..e69ca17892 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -41,7 +41,7 @@ public class DefaultRepositoryConfiguration { public static final String DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX = "Impl"; - private static final Key DEFAULT_QUERY_LOOKUP_STRATEGY = Key.CREATE_IF_NOT_FOUND; + public static final Key DEFAULT_QUERY_LOOKUP_STRATEGY = Key.CREATE_IF_NOT_FOUND; private final @NonNull T configurationSource; private final @NonNull BeanDefinition definition; diff --git a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java new file mode 100644 index 0000000000..a71a484870 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java @@ -0,0 +1,107 @@ +/* + * 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 lombok.Value; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.core.type.AnnotationMetadata; +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 + * @since 2.1 + */ +@Value(staticConstructor = "of") +public class FragmentMetadata { + + private String fragmentInterfaceName; + private RepositoryFragmentDiscovery configuration; + + /** + * Returns whether the given interface is a fragment candidate. + * + * @param interfaceName must not be {@literal null} or empty. + * @param factory must not be {@literal null}. + * @return + */ + public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) { + + Assert.hasText(interfaceName, "Interface name must not be null or empty!"); + Assert.notNull(factory, "MetadataReaderFactory must not be null!"); + + AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory); + + 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) { + + try { + return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata(); + } 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/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index 1d1500ba7d..dad778e539 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -15,42 +15,35 @@ */ package org.springframework.data.repository.config; -import lombok.Value; +import lombok.RequiredArgsConstructor; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanDefinitionStoreException; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.AnnotationMetadata; 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.config.ParsingUtils; -import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; import org.springframework.data.util.Optionals; -import org.springframework.data.util.StreamUtils; +import org.springframework.data.util.Streamable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances. @@ -180,23 +173,24 @@ private Stream registerRepositoryFragmentsImple RepositoryConfiguration configuration) { ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface()); + RepositoryFragmentDiscovery fragmentConfiguration = new DefaultRepositoryFragmentDiscovery(configuration); return Arrays.stream(classMetadata.getInterfaceNames()) // .filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) // - .map(it -> FragmentMetadata.of(it, configuration)) // - .map(it -> detectRepositoryFragmentConfiguration(it)) // - .flatMap(it -> Optionals.toStream(it)) // + .map(it -> FragmentMetadata.of(it, fragmentConfiguration)) // + .map(it -> detectRepositoryFragmentConfiguration(it, configuration.getConfigurationSource())) // + .flatMap(Optionals::toStream) // .peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) // .peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)); } private Optional detectRepositoryFragmentConfiguration( - FragmentMetadata configuration) { + FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) { String className = configuration.getFragmentImplementationClassName(); Optional beanDefinition = implementationDetector.detectCustomImplementation(className, null, - configuration.getBasePackages(), configuration.getExclusions(), configuration.getBeanNameGenerator()); + configuration.getBasePackages(), configuration.getExclusions(), configurationSource::generateBeanName); return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); } @@ -256,81 +250,27 @@ private ClassMetadata getClassMetadata(String className) { } } - @Value(staticConstructor = "of") - static class FragmentMetadata { + @RequiredArgsConstructor + private static class DefaultRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery { - String fragmentInterfaceName; - RepositoryConfiguration configuration; + private final RepositoryConfiguration configuration; - /** - * Returns whether the given interface is a fragment candidate. - * - * @param interfaceName must not be {@literal null} or empty. - * @param factory must not be {@literal null}. - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters() */ - public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) { - - Assert.hasText(interfaceName, "Interface name must not be null or empty!"); - Assert.notNull(factory, "MetadataReaderFactory must not be null!"); - - AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory); - - 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() { - - RepositoryConfigurationSource configurationSource = configuration.getConfigurationSource(); - String postfix = configurationSource.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)); + @Override + public Streamable getExcludeFilters() { + return configuration.getExcludeFilters(); } - /** - * Returns the bean name generating function to be used for the fragment. - * - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix() */ - public Function getBeanNameGenerator() { - return definition -> configuration.getConfigurationSource().generateBeanName(definition); - } - - private static AnnotationMetadata getAnnotationMetadata(String className, - MetadataReaderFactory metadataReaderFactory) { - - try { - return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata(); - } catch (IOException e) { - throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); - } + @Override + public Optional getRepositoryImplementationPostfix() { + return configuration.getConfigurationSource().getRepositoryImplementationPostfix(); } } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java new file mode 100644 index 0000000000..d249885987 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java @@ -0,0 +1,45 @@ +/* + * 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/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterface.java b/src/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterface.java new file mode 100644 index 0000000000..93536c5cf3 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterface.java @@ -0,0 +1,26 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +public interface AnotherFragmentInterface { + + int getPriority(); + + int getShadowed(); +} diff --git a/src/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterfaceImpl.java b/src/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterfaceImpl.java new file mode 100644 index 0000000000..1b7621f569 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/AnotherFragmentInterfaceImpl.java @@ -0,0 +1,32 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +class AnotherFragmentInterfaceImpl implements AnotherFragmentInterface { + + @Override + public int getPriority() { + return 2; + } + + @Override + public int getShadowed() { + return 2; + } +} diff --git a/src/test/java/org/springframework/data/repository/cdi/AnotherRepositoryImpl.java b/src/test/java/org/springframework/data/repository/cdi/AnotherRepositoryImpl.java index 33e5075f4e..b2fa86af1e 100644 --- a/src/test/java/org/springframework/data/repository/cdi/AnotherRepositoryImpl.java +++ b/src/test/java/org/springframework/data/repository/cdi/AnotherRepositoryImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * 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. @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.repository.cdi; -public class AnotherRepositoryImpl implements AnotherRepositoryCustom { +/** + * @author Mark Paluch + */ +class AnotherRepositoryImpl implements AnotherRepositoryCustom { @Override public int returnZero() { diff --git a/src/test/java/org/springframework/data/repository/cdi/CdiConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/repository/cdi/CdiConfigurationIntegrationTests.java new file mode 100644 index 0000000000..3de518d30f --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/CdiConfigurationIntegrationTests.java @@ -0,0 +1,58 @@ +/* + * 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.cdi; + +import static org.assertj.core.api.Assertions.*; + +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.data.repository.cdi.isolated.IsolatedComposedRepository; + +/** + * CDI integration tests for {@link CdiRepositoryConfiguration}. + * + * @author Mark Paluch + */ +public class CdiConfigurationIntegrationTests { + + private static SeContainer container; + + @BeforeClass + public static void setUp() { + + container = SeContainerInitializer.newInstance() // + .disableDiscovery() // + .addPackages(IsolatedComposedRepository.class) // + .initialize(); + } + + @Test // DATACMNS-1233 + public void shouldApplyImplementationPostfix() { + + IsolatedComposedRepository repository = container.select(IsolatedComposedRepository.class).get(); + + assertThat(repository.getPriority()).isEqualTo(42); + } + + @AfterClass + public static void tearDown() { + container.close(); + } +} 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 c2abd71156..7784df06c3 100755 --- a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java @@ -39,11 +39,18 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.DefaultEvaluationContextProvider; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; /** * Unit tests for {@link CdiRepositoryBean}. * * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class CdiRepositoryBeanUnitTests { @@ -55,6 +62,7 @@ public class CdiRepositoryBeanUnitTests { .singleton((Annotation) new CdiRepositoryExtensionSupport.DefaultAnnotationLiteral()); @Mock BeanManager beanManager; + @Mock RepositoryFactorySupport repositoryFactory; @Test(expected = IllegalArgumentException.class) public void voidRejectsNullQualifiers() { @@ -147,13 +155,32 @@ protected SampleRepository create( // verify(detector).detectCustomImplementation( // eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), // - eq("namedRepositoryImpl"), // + eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), // anySet(), // anySet(), // Mockito.any(Function.class) // ); } + @Test // DATACMNS-1233 + public void appliesRepositoryConfiguration() { + + DummyCdiRepositoryBean bean = new DummyCdiRepositoryBean(NO_ANNOTATIONS, + SampleRepository.class, beanManager) { + @Override + protected CdiRepositoryConfiguration lookupConfiguration(BeanManager beanManager, Set qualifiers) { + return RepositoryConfiguration.INSTANCE; + } + }; + + bean.applyConfiguration(repositoryFactory); + + verify(repositoryFactory).setEvaluationContextProvider(DefaultEvaluationContextProvider.INSTANCE); + verify(repositoryFactory).setNamedQueries(PropertiesBasedNamedQueries.EMPTY); + verify(repositoryFactory).setRepositoryBaseClass(String.class); + verify(repositoryFactory).setQueryLookupStrategyKey(Key.CREATE); + } + static class DummyCdiRepositoryBean extends CdiRepositoryBean { DummyCdiRepositoryBean(Set qualifiers, Class repositoryType, BeanManager beanManager) { @@ -168,12 +195,37 @@ protected T create(CreationalContext creationalContext, Class repositoryTy } @Named("namedRepository") - static interface SampleRepository extends Repository { + interface SampleRepository extends Repository { } @StereotypeAnnotation - static interface StereotypedSampleRepository { + interface StereotypedSampleRepository { + + } + + enum RepositoryConfiguration implements CdiRepositoryConfiguration { + INSTANCE; + + @Override + public Optional getEvaluationContextProvider() { + return Optional.of(DefaultEvaluationContextProvider.INSTANCE); + } + + @Override + public Optional getNamedQueries() { + return Optional.of(PropertiesBasedNamedQueries.EMPTY); + } + + @Override + public Optional getQueryLookupStrategy() { + return Optional.of(Key.CREATE); + } + + @Override + public Optional> getRepositoryBeanClass() { + return Optional.of(String.class); + } } } diff --git a/src/test/java/org/springframework/data/repository/cdi/ComposedRepository.java b/src/test/java/org/springframework/data/repository/cdi/ComposedRepository.java new file mode 100644 index 0000000000..eca46ab681 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/ComposedRepository.java @@ -0,0 +1,31 @@ +/* + * 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.cdi; + +import java.io.Serializable; + +import org.springframework.data.repository.Repository; + +/** + * @author Mark Paluch + */ +public interface ComposedRepository + extends Repository, FragmentInterface, AnotherFragmentInterface { + + // duplicate method shadowed by AnotherFragmentInterfaceImpl. The legacy custom implementation comes last, after all + // other fragments. + int getShadowed(); +} diff --git a/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryCustom.java b/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryCustom.java new file mode 100644 index 0000000000..c0e79b5f01 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryCustom.java @@ -0,0 +1,29 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +public interface ComposedRepositoryCustom { + + int returnFourtyTwo(); + + // duplicate method shadowed by AnotherFragmentInterfaceImpl. The legacy custom implementation comes last, after all + // other + // fragments. + int getShadowed(); +} diff --git a/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryImpl.java b/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryImpl.java new file mode 100644 index 0000000000..06b3fbb720 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/ComposedRepositoryImpl.java @@ -0,0 +1,32 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +class ComposedRepositoryImpl implements ComposedRepositoryCustom { + + @Override + public int returnFourtyTwo() { + return 42; + } + + @Override + public int getShadowed() { + return 1; + } +} diff --git a/src/test/java/org/springframework/data/repository/cdi/DummyCdiExtension.java b/src/test/java/org/springframework/data/repository/cdi/DummyCdiExtension.java index fd681dde52..f6eb4c240a 100644 --- a/src/test/java/org/springframework/data/repository/cdi/DummyCdiExtension.java +++ b/src/test/java/org/springframework/data/repository/cdi/DummyCdiExtension.java @@ -24,16 +24,15 @@ import java.util.Set; import javax.enterprise.context.NormalScope; -import javax.enterprise.context.spi.Contextual; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.AfterBeanDiscovery; import javax.enterprise.inject.spi.BeanManager; import org.apache.webbeans.context.AbstractContext; -import org.apache.webbeans.context.creational.BeanInstanceBag; import org.mockito.Mockito; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; +import org.springframework.data.repository.core.support.DummyRepositoryFactory; /** * Dummy extension of {@link CdiRepositoryExtensionSupport} to allow integration tests. Will create mocks for repository @@ -74,9 +73,12 @@ public Class getScope() { } @Override - protected T create(CreationalContext creationalContext, Class repositoryType, - Optional customImplementation) { - return Mockito.mock(repositoryType); + protected T create(CreationalContext creationalContext, Class repositoryType) { + + T mock = Mockito.mock(repositoryType); + DummyRepositoryFactory repositoryFactory = new DummyRepositoryFactory(mock); + + return create(repositoryFactory, repositoryType, getRepositoryFragments(repositoryType)); } } diff --git a/src/test/java/org/springframework/data/repository/cdi/FragmentInterface.java b/src/test/java/org/springframework/data/repository/cdi/FragmentInterface.java new file mode 100644 index 0000000000..5102c92a2e --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/FragmentInterface.java @@ -0,0 +1,23 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +public interface FragmentInterface { + int getPriority(); +} diff --git a/src/test/java/org/springframework/data/repository/cdi/FragmentInterfaceImpl.java b/src/test/java/org/springframework/data/repository/cdi/FragmentInterfaceImpl.java new file mode 100644 index 0000000000..912530af11 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/FragmentInterfaceImpl.java @@ -0,0 +1,27 @@ +/* + * 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.cdi; + +/** + * @author Mark Paluch + */ +class FragmentInterfaceImpl implements FragmentInterface { + + @Override + public int getPriority() { + return 1; + } +} diff --git a/src/test/java/org/springframework/data/repository/cdi/RepositoryFragmentsIntegrationTests.java b/src/test/java/org/springframework/data/repository/cdi/RepositoryFragmentsIntegrationTests.java new file mode 100644 index 0000000000..477679ef1c --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/RepositoryFragmentsIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * 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.cdi; + +import static org.assertj.core.api.Assertions.*; + +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * CDI integration tests for composed repositories. + * + * @author Mark Paluch + */ +public class RepositoryFragmentsIntegrationTests { + + private static SeContainer container; + + @BeforeClass + public static void setUp() { + + container = SeContainerInitializer.newInstance() // + .disableDiscovery() // + .addPackages(ComposedRepository.class) // + .initialize(); + } + + @Test // DATACMNS-1233 + public void shouldInvokeCustomImplementationLast() { + + ComposedRepository repository = getBean(ComposedRepository.class); + ComposedRepositoryImpl customImplementation = getBean(ComposedRepositoryImpl.class); + AnotherFragmentInterfaceImpl shadowed = getBean(AnotherFragmentInterfaceImpl.class); + + assertThat(repository.getShadowed()).isEqualTo(2); + assertThat(customImplementation.getShadowed()).isEqualTo(1); + assertThat(shadowed.getShadowed()).isEqualTo(2); + } + + @Test // DATACMNS-1233 + public void shouldRespectInterfaceOrder() { + + ComposedRepository repository = getBean(ComposedRepository.class); + FragmentInterfaceImpl fragment = getBean(FragmentInterfaceImpl.class); + AnotherFragmentInterfaceImpl shadowed = getBean(AnotherFragmentInterfaceImpl.class); + + assertThat(repository.getPriority()).isEqualTo(1); + assertThat(fragment.getPriority()).isEqualTo(1); + assertThat(shadowed.getPriority()).isEqualTo(2); + } + + protected T getBean(Class type) { + return container.select(type).get(); + } + + @AfterClass + public static void tearDown() { + container.close(); + } +} diff --git a/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterface.java b/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterface.java new file mode 100644 index 0000000000..5399c18137 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterface.java @@ -0,0 +1,24 @@ +/* + * 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.cdi.isolated; + +/** + * @author Mark Paluch + */ +public interface FragmentInterface { + + int getPriority(); +} diff --git a/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterfaceFoo.java b/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterfaceFoo.java new file mode 100644 index 0000000000..79d134587b --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/isolated/FragmentInterfaceFoo.java @@ -0,0 +1,27 @@ +/* + * 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.cdi.isolated; + +/** + * @author Mark Paluch + */ +class FragmentInterfaceFoo implements FragmentInterface { + + @Override + public int getPriority() { + return 42; + } +} diff --git a/src/test/java/org/springframework/data/repository/cdi/isolated/IsolatedComposedRepository.java b/src/test/java/org/springframework/data/repository/cdi/isolated/IsolatedComposedRepository.java new file mode 100644 index 0000000000..6d039a2f13 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/isolated/IsolatedComposedRepository.java @@ -0,0 +1,25 @@ +/* + * 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.cdi.isolated; + +import java.io.Serializable; + +import org.springframework.data.repository.Repository; + +/** + * @author Mark Paluch + */ +public interface IsolatedComposedRepository extends Repository, FragmentInterface {} diff --git a/src/test/java/org/springframework/data/repository/cdi/isolated/MyCdiConfiguration.java b/src/test/java/org/springframework/data/repository/cdi/isolated/MyCdiConfiguration.java new file mode 100644 index 0000000000..e2be6b7395 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/cdi/isolated/MyCdiConfiguration.java @@ -0,0 +1,29 @@ +/* + * 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.cdi.isolated; + +import org.springframework.data.repository.cdi.CdiRepositoryConfiguration; + +/** + * @author Mark Paluch + */ +public class MyCdiConfiguration implements CdiRepositoryConfiguration { + + @Override + public String getRepositoryImplementationPostfix() { + return "Foo"; + } +}