package org.springframework.data.repository.cdi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
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.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.PassivationCapable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
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.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapable {
private static final Logger LOGGER = LoggerFactory.getLogger(CdiRepositoryBean.class);
private static final CdiRepositoryConfiguration DEFAULT_CONFIGURATION = DefaultCdiRepositoryConfiguration.INSTANCE;
private final Set<Annotation> qualifiers;
private final Class<T> repositoryType;
private final CdiRepositoryContext context;
private final BeanManager beanManager;
private final String passivationId;
private transient @Nullable T repoInstance;
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager) {
this(qualifiers, repositoryType, beanManager, new CdiRepositoryContext(CdiRepositoryBean.class.getClassLoader()));
}
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager,
Optional<CustomRepositoryImplementationDetector> detector) {
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 = new CdiRepositoryContext(getClass().getClassLoader(), detector
.orElseThrow(() -> new IllegalArgumentException("CustomRepositoryImplementationDetector must be present!")));
this.passivationId = createPassivationId(qualifiers, repositoryType);
}
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);
}
private String createPassivationId(Set<Annotation> qualifiers, Class<?> repositoryType) {
List<String> qualifierNames = new ArrayList<>(qualifiers.size());
for (Annotation qualifier : qualifiers) {
qualifierNames.add(qualifier.annotationType().getName());
}
Collections.sort(qualifierNames);
return StringUtils.collectionToDelimitedString(qualifierNames, ":") + ":" + repositoryType.getName();
}
@SuppressWarnings("rawtypes")
public Set<Type> getTypes() {
Set<Class> interfaces = new HashSet<>();
interfaces.add(repositoryType);
interfaces.addAll(Arrays.asList(repositoryType.getInterfaces()));
return new HashSet<>(interfaces);
}
protected <S> S getDependencyInstance(Bean<S> bean) {
return getDependencyInstance(bean, bean.getBeanClass());
}
@SuppressWarnings("unchecked")
protected <S> S getDependencyInstance(Bean<S> bean, Class<?> type) {
CreationalContext<S> creationalContext = beanManager.createCreationalContext(bean);
return (S) beanManager.getReference(bean, type, creationalContext);
}
public final void initialize() {
create(beanManager.createCreationalContext(this));
}
public final T create(@SuppressWarnings("null") CreationalContext<T> creationalContext) {
T repoInstance = this.repoInstance;
if (repoInstance != null) {
LOGGER.debug("Returning eagerly created CDI repository instance for {}.", repositoryType.getName());
return repoInstance;
}
LOGGER.debug("Creating CDI repository bean instance for {}.", repositoryType.getName());
repoInstance = create(creationalContext, repositoryType);
this.repoInstance = repoInstance;
return repoInstance;
}
public void destroy(@SuppressWarnings("null") T instance,
@SuppressWarnings("null") CreationalContext<T> creationalContext) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("Destroying bean instance %s for repository type '%s'.", instance.toString(),
repositoryType.getName()));
}
creationalContext.release();
}
public Set<Annotation> getQualifiers() {
return qualifiers;
}
public String getName() {
return repositoryType.getName();
}
public Set<Class<? extends Annotation>> getStereotypes() {
return Arrays.stream(repositoryType.getAnnotations())
.map(Annotation::annotationType)
.filter(it -> it.isAnnotationPresent(Stereotype.class))
.collect(Collectors.toSet());
}
public Class<?> getBeanClass() {
return repositoryType;
}
public boolean isAlternative() {
return repositoryType.isAnnotationPresent(Alternative.class);
}
public boolean isNullable() {
return false;
}
public Set<InjectionPoint> getInjectionPoints() {
return Collections.emptySet();
}
public Class<? extends Annotation> getScope() {
return ApplicationScoped.class;
}
public String getId() {
return passivationId;
}
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
cdiRepositoryConfiguration);
Optional<Object> customImplementation = customImplementationBean.map(this::getDependencyInstance);
return create(creationalContext, repositoryType, customImplementation);
}
protected T create(Supplier<? extends RepositoryFactorySupport> factorySupplier, Class<T> repositoryType) {
CdiRepositoryConfiguration configuration = lookupConfiguration(beanManager, qualifiers);
RepositoryFragments repositoryFragments = getRepositoryFragments(repositoryType, configuration);
RepositoryFactorySupport factory = factorySupplier.get();
applyConfiguration(factory, configuration);
return create(factory, repositoryType, repositoryFragments);
}
protected RepositoryFragments getRepositoryFragments(Class<T> repositoryType) {
Assert.notNull(repositoryType, "Repository type must not be null!");
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
return getRepositoryFragments(repositoryType, cdiRepositoryConfiguration);
}
private RepositoryFragments getRepositoryFragments(Class<T> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
cdiRepositoryConfiguration);
Optional<Object> customImplementation = customImplementationBean.map(this::getDependencyInstance);
List<RepositoryFragment<?>> repositoryFragments = findRepositoryFragments(repositoryType,
cdiRepositoryConfiguration);
RepositoryFragments customImplementationFragment = customImplementation
.map(RepositoryFragments::just)
.orElseGet(RepositoryFragments::empty);
return RepositoryFragments.from(repositoryFragments)
.append(customImplementationFragment);
}
@SuppressWarnings("unchecked")
private List<RepositoryFragment<?>> findRepositoryFragments(Class<T> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
Stream<RepositoryFragmentConfiguration> fragmentConfigurations = context
.getRepositoryFragments(cdiRepositoryConfiguration, repositoryType);
return fragmentConfigurations.flatMap(it -> {
Class<Object> interfaceClass = (Class<Object>) lookupFragmentInterface(repositoryType, it.getInterfaceName());
Class<?> implementationClass = context.loadClass(it.getClassName());
Optional<Bean<?>> bean = getBean(implementationClass, beanManager, qualifiers);
return Optionals.toStream(bean.map(this::getDependencyInstance)
.map(implementation -> RepositoryFragment.implemented(interfaceClass, implementation)));
}).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()))));
}
@Deprecated
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType,
Optional<Object> customImplementation) {
throw new UnsupportedOperationException(
"You have to implement create(CreationalContext<T>, Class<T>, Optional<Object>) "
+ "in order to use custom repository implementations");
}
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);
}
private Optional<Bean<?>> getCustomImplementationBean(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
return context.getCustomImplementationClass(repositoryType, cdiRepositoryConfiguration)
.flatMap(type -> getBean(type, beanManager, qualifiers));
}
protected void applyConfiguration(RepositoryFactorySupport repositoryFactory) {
applyConfiguration(repositoryFactory, lookupConfiguration(beanManager, qualifiers));
}
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);
configuration.getRepositoryProxyPostProcessors().forEach(repositoryFactory::addRepositoryProxyPostProcessor);
configuration.getQueryCreationListeners().forEach(repositoryFactory::addQueryCreationListener);
}
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()]);
}
@Override
public String toString() {
return String.format("CdiRepositoryBean: type='%s', qualifiers=%s", repositoryType.getName(),
qualifiers.toString());
}
enum DefaultCdiRepositoryConfiguration implements CdiRepositoryConfiguration {
INSTANCE;
}
}