DATACMNS-1233 - Allow CDI Repositories to be composed of an arbitrary number of implementation classes.
We now support repository fragments for repositories exported through CDI. Original pull request: #272.
This commit is contained in:
committed by
Oliver Gierke
parent
919090bf20
commit
0a3b71f0a1
@@ -25,12 +25,12 @@ import java.util.List;
|
||||
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 javax.enterprise.inject.spi.PassivationCapable;
|
||||
|
||||
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.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<T> implements Bean<T>, PassivationCapabl
|
||||
|
||||
private final Set<Annotation> qualifiers;
|
||||
private final Class<T> repositoryType;
|
||||
private final Optional<CustomRepositoryImplementationDetector> 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<T> implements Bean<T>, PassivationCapabl
|
||||
* @param beanManager the CDI {@link BeanManager}, must not be {@literal null}.
|
||||
*/
|
||||
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager) {
|
||||
this(qualifiers, repositoryType, beanManager, Optional.empty());
|
||||
this(qualifiers, repositoryType, beanManager, new CdiRepositoryContext(CdiRepositoryBean.class.getClassLoader()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +87,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
* @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<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager,
|
||||
Optional<CustomRepositoryImplementationDetector> detector) {
|
||||
@@ -107,7 +100,32 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
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<Annotation> qualifiers, Class<T> 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 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
|
||||
Collections.sort(qualifierNames);
|
||||
return StringUtils.collectionToDelimitedString(qualifierNames, ":") + ":" + repositoryType.getName();
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -215,89 +232,6 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
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<Annotation> 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<Bean<?>> getCustomImplementationBean(Class<?> repositoryType, BeanManager beanManager,
|
||||
Set<Annotation> 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<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
|
||||
CdiRepositoryConfiguration cdiRepositoryConfiguration, CustomRepositoryImplementationDetector detector) {
|
||||
|
||||
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
|
||||
Optional<AbstractBeanDefinition> 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<Annotation> qualifiers) {
|
||||
return qualifiers.toArray(new Annotation[qualifiers.size()]);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see javax.enterprise.inject.spi.Bean#getQualifiers()
|
||||
@@ -380,18 +314,75 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
* @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<T> creationalContext, Class<T> repositoryType) {
|
||||
|
||||
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType, beanManager, qualifiers);
|
||||
Optional<Object> customImplementation = customImplementationBean
|
||||
.map(it -> beanManager.getReference(it, it.getBeanClass(), beanManager.createCreationalContext(it)));
|
||||
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
|
||||
|
||||
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
|
||||
cdiRepositoryConfiguration);
|
||||
Optional<Object> 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<T> repositoryType) {
|
||||
|
||||
Assert.notNull(repositoryType, "Repository type must not be null!");
|
||||
|
||||
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
|
||||
|
||||
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
|
||||
cdiRepositoryConfiguration);
|
||||
Optional<Object> customImplementation = customImplementationBean.map(this::getDependencyInstance);
|
||||
|
||||
List<RepositoryFragment<?>> repositoryFragments = getRepositoryFragments(repositoryType,
|
||||
cdiRepositoryConfiguration);
|
||||
|
||||
RepositoryFragments customImplementationFragment = customImplementation //
|
||||
.map(RepositoryFragments::just) //
|
||||
.orElseGet(RepositoryFragments::empty);
|
||||
|
||||
return RepositoryFragments.from(repositoryFragments) //
|
||||
.append(customImplementationFragment);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<RepositoryFragment<?>> getRepositoryFragments(Class<T> repositoryType,
|
||||
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
|
||||
|
||||
Stream<RepositoryFragmentConfiguration> fragmentConfigurations = context
|
||||
.getRepositoryFragments(cdiRepositoryConfiguration, repositoryType);
|
||||
|
||||
return fragmentConfigurations.flatMap(it -> {
|
||||
|
||||
Class<Object> interfaceClass = (Class) lookupFragmentInterface(repositoryType, it.getInterfaceName());
|
||||
Class<?> implementationClass = context.loadClass(it.getClassName());
|
||||
Optional<Bean<?>> 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 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
* @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<T> creationalContext, Class<T> repositoryType,
|
||||
Optional<Object> customImplementation) {
|
||||
throw new UnsupportedOperationException(
|
||||
@@ -407,6 +401,35 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
+ "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<Annotation> 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<Bean<?>> 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.
|
||||
@@ -435,6 +458,26 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
|
||||
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> T create(RepositoryFactorySupport repositoryFactory, Class<T> repositoryType,
|
||||
RepositoryFragments repositoryFragments) {
|
||||
return repositoryFactory.getRepository(repositoryType, repositoryFragments);
|
||||
}
|
||||
|
||||
private static Optional<Bean<?>> getBean(Class<?> beanType, BeanManager beanManager, Set<Annotation> qualifiers) {
|
||||
return beanManager.getBeans(beanType, getQualifiersArray(qualifiers)).stream().findFirst();
|
||||
}
|
||||
|
||||
private static Annotation[] getQualifiersArray(Set<Annotation> qualifiers) {
|
||||
return qualifiers.toArray(new Annotation[qualifiers.size()]);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
|
||||
@@ -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<RepositoryFragmentConfiguration> 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<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
|
||||
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
|
||||
|
||||
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
|
||||
|
||||
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
|
||||
className, //
|
||||
className, Collections.singleton(repositoryType.getPackage().getName()), //
|
||||
Collections.emptySet(), //
|
||||
BeanDefinition::getBeanClassName);
|
||||
|
||||
return beanDefinition.map(it -> loadClass(it.getBeanClassName()));
|
||||
}
|
||||
|
||||
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
|
||||
FragmentMetadata configuration) {
|
||||
|
||||
String className = configuration.getFragmentImplementationClassName();
|
||||
|
||||
Optional<AbstractBeanDefinition> 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<TypeFilter> getExcludeFilters() {
|
||||
return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getRepositoryImplementationPostfix() {
|
||||
return Optional.of(configuration.getRepositoryImplementationPostfix());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,6 @@ import javax.inject.Qualifier;
|
||||
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<Class<?>, Set<Annotation>> repositoryTypes = new HashMap<>();
|
||||
private final Set<CdiRepositoryBean<?>> 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 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
|
||||
Set<Annotation> 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 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
|
||||
* @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")
|
||||
|
||||
@@ -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<TypeFilter> getExclusions() {
|
||||
|
||||
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
|
||||
Stream<AnnotationTypeFilter> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@ class RepositoryBeanDefinitionBuilder {
|
||||
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<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
|
||||
FragmentMetadata configuration) {
|
||||
FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) {
|
||||
|
||||
String className = configuration.getFragmentImplementationClassName();
|
||||
|
||||
Optional<AbstractBeanDefinition> 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 @@ class RepositoryBeanDefinitionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
@Override
|
||||
public Streamable<TypeFilter> getExcludeFilters() {
|
||||
return configuration.getExcludeFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exclusions to be used when scanning for fragment implementations.
|
||||
*
|
||||
* @return
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
|
||||
*/
|
||||
public List<TypeFilter> getExclusions() {
|
||||
|
||||
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
|
||||
Stream<AnnotationTypeFilter> 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<String> getBasePackages() {
|
||||
return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bean name generating function to be used for the fragment.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Function<BeanDefinition, String> 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<String> getRepositoryImplementationPostfix() {
|
||||
return configuration.getConfigurationSource().getRepositoryImplementationPostfix();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TypeFilter> 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<String> getRepositoryImplementationPostfix();
|
||||
}
|
||||
Reference in New Issue
Block a user