DATACMNS-102 - Allow Repositories to be composed of an arbitrary number of implementation classes.
We now use RepositoryComposition as backing implementation for repository method calls to implementations. We now scan for repository fragments during the repository configuration phase. Fragment implementation candidates derive from the repository interface declaration, specifically the declared interfaces and their order. We scan the class path during fragment scan for each interface and add discovered fragments to the repository composition. The name of the implementation is derived from the simple name of the interface and the implementation suffix. Qualified fragments are top-level interface declarations that are not annotated with NoRepositoryBean. Inherited interfaces are not considered as fragment candidates. We create a RepositoryComposition from the discovered fragments in the order of interface declaration in the repository interface and supply the composition to the actual repository creation. Original pull request: #222.
This commit is contained in:
committed by
Oliver Gierke
parent
5e123ad7df
commit
92e8907030
@@ -15,20 +15,34 @@
|
||||
*/
|
||||
package org.springframework.data.repository.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.ClassMetadata;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.core.type.filter.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.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances.
|
||||
@@ -36,6 +50,7 @@ import org.springframework.util.Assert;
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
* @author Peter Rietzler
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class RepositoryBeanDefinitionBuilder {
|
||||
|
||||
@@ -106,6 +121,19 @@ class RepositoryBeanDefinitionBuilder {
|
||||
builder.addDependsOn(it);
|
||||
});
|
||||
|
||||
BeanDefinitionBuilder fragmentsBuilder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(RepositoryFragmentsFactoryBean.class);
|
||||
|
||||
List<String> fragmentBeanNames = registerRepositoryFragmentsImplementation(configuration) //
|
||||
.stream() //
|
||||
.map(RepositoryFragmentConfiguration::getFragmentBeanName) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
fragmentsBuilder.addConstructorArgValue(fragmentBeanNames);
|
||||
|
||||
builder.addPropertyValue("repositoryFragments",
|
||||
ParsingUtils.getSourceBeanDefinition(fragmentsBuilder, configuration.getSource()));
|
||||
|
||||
RootBeanDefinition evaluationContextProviderDefinition = new RootBeanDefinition(
|
||||
ExtensionAwareEvaluationContextProvider.class);
|
||||
evaluationContextProviderDefinition.setSource(configuration.getSource());
|
||||
@@ -140,4 +168,99 @@ class RepositoryBeanDefinitionBuilder {
|
||||
return beanName;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private List<RepositoryFragmentConfiguration> registerRepositoryFragmentsImplementation(
|
||||
RepositoryConfiguration<?> configuration) {
|
||||
|
||||
ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface());
|
||||
|
||||
return Arrays.stream(classMetadata.getInterfaceNames())
|
||||
.map(it -> detectRepositoryFragmentConfiguration(configuration, it)) //
|
||||
.filter(Optional::isPresent) //
|
||||
.map(Optional::get) //
|
||||
.peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) //
|
||||
.peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
|
||||
RepositoryConfiguration<?> configuration, String fragmentInterfaceName) {
|
||||
|
||||
List<TypeFilter> exclusions = getExclusions(configuration);
|
||||
|
||||
String className = ClassUtils.getShortName(fragmentInterfaceName)
|
||||
.concat(configuration.getConfigurationSource().getRepositoryImplementationPostfix().orElse("Impl"));
|
||||
|
||||
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(className, null,
|
||||
configuration.getBasePackages(), exclusions, bd -> configuration.getConfigurationSource().generateBeanName(bd));
|
||||
|
||||
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterfaceName, bd));
|
||||
}
|
||||
|
||||
private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration<?> repositoryConfiguration,
|
||||
RepositoryFragmentConfiguration fragmentConfiguration) {
|
||||
|
||||
String beanName = fragmentConfiguration.getImplementationBeanName();
|
||||
|
||||
// Already a bean configured?
|
||||
if (registry.containsBeanDefinition(beanName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("Registering repository fragment implementation: %s %s", beanName,
|
||||
fragmentConfiguration.getClassName()));
|
||||
}
|
||||
|
||||
fragmentConfiguration.getBeanDefinition().ifPresent(bd -> {
|
||||
|
||||
bd.setSource(repositoryConfiguration.getSource());
|
||||
|
||||
registry.registerBeanDefinition(beanName, bd);
|
||||
});
|
||||
}
|
||||
|
||||
private void potentiallyRegisterRepositoryFragment(RepositoryConfiguration<?> configuration,
|
||||
RepositoryFragmentConfiguration fragmentConfiguration) {
|
||||
|
||||
String beanName = fragmentConfiguration.getFragmentBeanName();
|
||||
|
||||
// Already a bean configured?
|
||||
if (registry.containsBeanDefinition(beanName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Registering repository fragment: " + beanName);
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder fragmentBuilder = BeanDefinitionBuilder.rootBeanDefinition(RepositoryFragment.class,
|
||||
"implemented");
|
||||
|
||||
fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName());
|
||||
fragmentBuilder.addConstructorArgReference(fragmentConfiguration.getImplementationBeanName());
|
||||
|
||||
registry.registerBeanDefinition(beanName,
|
||||
ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource()));
|
||||
}
|
||||
|
||||
private ClassMetadata getClassMetadata(String className) {
|
||||
try {
|
||||
return metadataReaderFactory.getMetadataReader(className).getClassMetadata();
|
||||
} catch (IOException e) {
|
||||
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TypeFilter> getExclusions(RepositoryConfiguration<?> configuration) {
|
||||
|
||||
List<TypeFilter> exclusions = new ArrayList<>();
|
||||
|
||||
for (TypeFilter typeFilter : configuration.getExcludeFilters()) {
|
||||
exclusions.add(typeFilter);
|
||||
}
|
||||
|
||||
exclusions.add(new AnnotationTypeFilter(NoRepositoryBean.class));
|
||||
return exclusions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
* Configuration information for a single repository instance.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public interface RepositoryConfiguration<T extends RepositoryConfigurationSource> {
|
||||
|
||||
@@ -60,14 +61,18 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
|
||||
* Returns the class name of the custom implementation.
|
||||
*
|
||||
* @return
|
||||
* @deprecated since 2.0. Use repository compositions by creating mixins.
|
||||
*/
|
||||
@Deprecated
|
||||
String getImplementationClassName();
|
||||
|
||||
/**
|
||||
* Returns the bean name of the custom implementation.
|
||||
*
|
||||
* @return
|
||||
* @deprecated since 2.0. Use repository compositions by creating mixins.
|
||||
*/
|
||||
@Deprecated
|
||||
String getImplementationBeanName();
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2017 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.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Fragment configuration consisting of an interface name and the implementation class name.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
@Value
|
||||
public class RepositoryFragmentConfiguration {
|
||||
|
||||
String interfaceName;
|
||||
String className;
|
||||
Optional<AbstractBeanDefinition> beanDefinition;
|
||||
|
||||
/**
|
||||
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@code className} of the
|
||||
* implementation.
|
||||
*
|
||||
* @param interfaceName must not be {@literal null} or empty.
|
||||
* @param className must not be {@literal null} or empty.
|
||||
*/
|
||||
public RepositoryFragmentConfiguration(String interfaceName, String className) {
|
||||
|
||||
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
|
||||
Assert.hasText(className, "Class name must not be null or empty!");
|
||||
|
||||
this.interfaceName = interfaceName;
|
||||
this.className = className;
|
||||
this.beanDefinition = Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@link AbstractBeanDefinition} of
|
||||
* the implementation.
|
||||
*
|
||||
* @param interfaceName must not be {@literal null} or empty.
|
||||
* @param beanDefinition must not be {@literal null}.
|
||||
*/
|
||||
public RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition) {
|
||||
|
||||
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
|
||||
Assert.notNull(beanDefinition, "Bean definition must not be null!");
|
||||
|
||||
this.interfaceName = interfaceName;
|
||||
this.beanDefinition = Optional.of(beanDefinition);
|
||||
this.className = beanDefinition.getBeanClassName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return name of the implementation bean.
|
||||
*/
|
||||
public String getImplementationBeanName() {
|
||||
return StringUtils.uncapitalize(ClassUtils.getShortName(getClassName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return name of the implementation fragment bean.
|
||||
*/
|
||||
public String getFragmentBeanName() {
|
||||
return getImplementationBeanName() + "Fragment";
|
||||
}
|
||||
}
|
||||
@@ -15,40 +15,29 @@
|
||||
*/
|
||||
package org.springframework.data.repository.core.support;
|
||||
|
||||
import static org.springframework.core.GenericTypeResolver.*;
|
||||
import static org.springframework.data.repository.util.ClassUtils.*;
|
||||
import static org.springframework.util.ReflectionUtils.*;
|
||||
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.annotation.QueryAnnotation;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.CrudMethods;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.util.Optionals;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RepositoryInformation}.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Mark Paluch
|
||||
@@ -56,34 +45,33 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
|
||||
@SuppressWarnings("rawtypes") //
|
||||
private static final TypeVariable<Class<Repository>>[] PARAMETERS = Repository.class.getTypeParameters();
|
||||
private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName();
|
||||
private static final String ID_TYPE_NAME = PARAMETERS[1].getName();
|
||||
|
||||
private final Map<Method, Method> methodCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final RepositoryMetadata metadata;
|
||||
private final Class<?> repositoryBaseClass;
|
||||
private final Optional<Class<?>> customImplementationClass;
|
||||
private final RepositoryComposition composition;
|
||||
private final RepositoryComposition baseComposition;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class.
|
||||
*
|
||||
*
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param repositoryBaseClass must not be {@literal null}.
|
||||
* @param customImplementationClass must not be {@literal null}.
|
||||
* @param composition must not be {@literal null}.
|
||||
*/
|
||||
public DefaultRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass,
|
||||
Optional<Class<?>> customImplementationClass) {
|
||||
RepositoryComposition composition) {
|
||||
|
||||
Assert.notNull(metadata, "Repository metadata must not be null!");
|
||||
Assert.notNull(repositoryBaseClass, "Repository base class must not be null!");
|
||||
Assert.notNull(customImplementationClass, "Custom implementation class must not be null!");
|
||||
Assert.notNull(composition, "Repository composition must not be null!");
|
||||
|
||||
this.metadata = metadata;
|
||||
this.repositoryBaseClass = repositoryBaseClass;
|
||||
this.customImplementationClass = customImplementationClass;
|
||||
this.composition = composition;
|
||||
this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) //
|
||||
.withArgumentConverter(composition.getArgumentConverter()) //
|
||||
.withMethodLookup(composition.getMethodLookup());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -124,13 +112,13 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return methodCache.get(method);
|
||||
}
|
||||
|
||||
Method result = getTargetClassMethod(method, customImplementationClass);
|
||||
Method result = composition.findMethod(method).orElse(method);
|
||||
|
||||
if (!result.equals(method)) {
|
||||
return cacheAndReturn(method, result);
|
||||
}
|
||||
|
||||
return cacheAndReturn(method, getTargetClassMethod(method, Optional.of(repositoryBaseClass)));
|
||||
return cacheAndReturn(method, baseComposition.findMethod(method).orElse(method));
|
||||
}
|
||||
|
||||
private Method cacheAndReturn(Method key, Method value) {
|
||||
@@ -143,20 +131,6 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given method is considered to be a repository base class method.
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private boolean isTargetClassMethod(Method method, Optional<Class<?>> targetType) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
return targetType.map(it -> method.getDeclaringClass().isAssignableFrom(it)
|
||||
|| !method.equals(getTargetClassMethod(method, targetType))).orElse(false);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.support.RepositoryInformation#getQueryMethods()
|
||||
@@ -178,7 +152,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
|
||||
/**
|
||||
* Checks whether the given method is a query method candidate.
|
||||
*
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
@@ -191,7 +165,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
/**
|
||||
* Checks whether the given method contains a custom store specific query annotation annotated with
|
||||
* {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation.
|
||||
*
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
@@ -206,10 +180,10 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
*/
|
||||
@Override
|
||||
public boolean isCustomMethod(Method method) {
|
||||
return isTargetClassMethod(method, customImplementationClass);
|
||||
return composition.findMethod(method).isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryInformation#isQueryMethod(java.lang.reflect.Method)
|
||||
*/
|
||||
@@ -226,30 +200,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
public boolean isBaseClassMethod(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
return isTargetClassMethod(method, Optional.of(repositoryBaseClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given target class' method if the given method (declared in the repository interface) was also declared
|
||||
* at the target class. Returns the given method if the given base class does not declare the method given. Takes
|
||||
* generics into account.
|
||||
*
|
||||
* @param method must not be {@literal null}
|
||||
* @param baseClass
|
||||
* @return
|
||||
*/
|
||||
Method getTargetClassMethod(Method method, Optional<Class<?>> baseClass) {
|
||||
|
||||
Supplier<Optional<Method>> directMatch = () -> baseClass
|
||||
.map(it -> findMethod(it, method.getName(), method.getParameterTypes()));
|
||||
|
||||
Supplier<Optional<Method>> detailedComparison = () -> baseClass.flatMap(it -> Arrays.stream(it.getMethods())//
|
||||
.filter(baseClassMethod -> method.getName().equals(baseClassMethod.getName()))// Right name
|
||||
.filter(baseClassMethod -> method.getParameterCount() == baseClassMethod.getParameterCount())
|
||||
.filter(baseClassMethod -> parametersMatch(method, baseClassMethod))// All parameters match
|
||||
.findFirst());
|
||||
|
||||
return Optionals.firstNonEmpty(directMatch, detailedComparison).orElse(method);
|
||||
return baseComposition.findMethod(method).isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -275,7 +226,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#getRepositoryInterface()
|
||||
*/
|
||||
@@ -284,7 +235,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return metadata.getRepositoryInterface();
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method)
|
||||
*/
|
||||
@@ -293,7 +244,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return metadata.getReturnedDomainClass(method);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#getCrudMethods()
|
||||
*/
|
||||
@@ -302,7 +253,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return metadata.getCrudMethods();
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#isPagingRepository()
|
||||
*/
|
||||
@@ -311,7 +262,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return metadata.isPagingRepository();
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#getAlternativeDomainTypes()
|
||||
*/
|
||||
@@ -320,7 +271,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
return metadata.getAlternativeDomainTypes();
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.RepositoryMetadata#isReactiveRepository()
|
||||
*/
|
||||
@@ -328,83 +279,4 @@ class DefaultRepositoryInformation implements RepositoryInformation {
|
||||
public boolean isReactiveRepository() {
|
||||
return metadata.isReactiveRepository();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK} is
|
||||
* declared, the method ensures that given method parameter is the primary key type declared in the given repository
|
||||
* interface e.g.
|
||||
*
|
||||
* @param variable must not be {@literal null}.
|
||||
* @param parameterType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected boolean matchesGenericType(TypeVariable<?> variable, ResolvableType parameterType) {
|
||||
|
||||
GenericDeclaration declaration = variable.getGenericDeclaration();
|
||||
|
||||
if (declaration instanceof Class) {
|
||||
|
||||
ResolvableType entityType = ResolvableType.forClass(getDomainType());
|
||||
ResolvableType idClass = ResolvableType.forClass(getIdType());
|
||||
|
||||
if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idClass)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Type boundType = variable.getBounds()[0];
|
||||
String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString();
|
||||
|
||||
return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType);
|
||||
}
|
||||
|
||||
for (Type type : variable.getBounds()) {
|
||||
if (ResolvableType.forType(type).isAssignableFrom(parameterType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
|
||||
* against the ones bound in the given repository interface.
|
||||
*
|
||||
* @param method
|
||||
* @param baseClassMethod
|
||||
* @return
|
||||
*/
|
||||
private boolean parametersMatch(Method method, Method baseClassMethod) {
|
||||
|
||||
Class<?>[] methodParameterTypes = method.getParameterTypes();
|
||||
Type[] genericTypes = baseClassMethod.getGenericParameterTypes();
|
||||
Class<?>[] types = baseClassMethod.getParameterTypes();
|
||||
|
||||
for (int i = 0; i < genericTypes.length; i++) {
|
||||
|
||||
Type genericType = genericTypes[i];
|
||||
Class<?> type = types[i];
|
||||
MethodParameter parameter = new MethodParameter(method, i);
|
||||
Class<?> parameterType = resolveParameterType(parameter, metadata.getRepositoryInterface());
|
||||
|
||||
if (genericType instanceof TypeVariable<?>) {
|
||||
|
||||
if (!matchesGenericType((TypeVariable<?>) genericType, ResolvableType.forMethodParameter(parameter))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (types[i].equals(parameterType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2017 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.core.support;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Strategy interface providing {@link MethodPredicate predicates} to resolve a method called on a composite to its
|
||||
* implementation method.
|
||||
* <p />
|
||||
* {@link MethodPredicate Predicates} are ordered by filtering priority and applied individually. If a predicate does
|
||||
* not yield any positive match, the next predicate is applied.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see RepositoryComposition
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MethodLookup {
|
||||
|
||||
/**
|
||||
* Return an ordered {@link List} of {@link MethodPredicate}. Each predicate is applied individually. If any
|
||||
* {@link MethodPredicate} matches, the tested candidate {@link Method} passes the filter.
|
||||
*
|
||||
* @return {@link List} of {@link MethodPredicate}.
|
||||
*/
|
||||
List<MethodPredicate> getLookups();
|
||||
|
||||
/**
|
||||
* Returns a composed {@link MethodLookup} that represents a concatenation of this predicate and another. When
|
||||
* evaluating the composed method lookup, if this lookup evaluates {@code true}, then the {@code other} method lookup
|
||||
* is not evaluated.
|
||||
*
|
||||
* @param other must not be {@literal null}.
|
||||
* @return the composed {@link MethodLookup}.
|
||||
*/
|
||||
default MethodLookup and(MethodLookup other) {
|
||||
|
||||
Assert.notNull(other, "Other method lookup must not be null!");
|
||||
|
||||
return () -> Stream.concat(getLookups().stream(), other.getLookups().stream()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* A method predicate to be applied on the {@link InvokedMethod} and {@link Method method candidate}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface MethodPredicate extends BiPredicate<InvokedMethod, Method> {
|
||||
|
||||
@Override
|
||||
boolean test(InvokedMethod invokedMethod, Method candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing an invoked {@link Method}.
|
||||
*/
|
||||
@Value(staticConstructor = "of")
|
||||
class InvokedMethod {
|
||||
|
||||
private final @NonNull Method method;
|
||||
|
||||
public Class<?> getDeclaringClass() {
|
||||
return method.getDeclaringClass();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return method.getName();
|
||||
}
|
||||
|
||||
public Class<?>[] getParameterTypes() {
|
||||
return method.getParameterTypes();
|
||||
}
|
||||
|
||||
public int getParameterCount() {
|
||||
return method.getParameterCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* Copyright 2017 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.core.support;
|
||||
|
||||
import static org.springframework.core.GenericTypeResolver.*;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.data.repository.util.ReactiveWrapperConverters;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementations of method lookup functions.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public abstract class MethodLookups {
|
||||
|
||||
private MethodLookups() {}
|
||||
|
||||
/**
|
||||
* Direct method lookup filtering on exact method name, parameter count and parameter types.
|
||||
*
|
||||
* @return direct method lookup.
|
||||
*/
|
||||
public static MethodLookup direct() {
|
||||
|
||||
MethodPredicate direct = (invoked, candidate) -> candidate.getName().equals(invoked.getName())
|
||||
&& candidate.getParameterCount() == invoked.getParameterCount()
|
||||
&& Arrays.equals(candidate.getParameterTypes(), invoked.getParameterTypes());
|
||||
|
||||
return () -> Collections.singletonList(direct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository type-aware method lookup composed of {@link #direct()} and {@link RepositoryAwareMethodLookup}.
|
||||
* <p/>
|
||||
* Repository-aware lookups resolve generic types from the repository declaration to verify assignability to Id/domain
|
||||
* types. This lookup also permits assignable method signatures but prefers {@link #direct()} matches.
|
||||
*
|
||||
* @param repositoryMetadata must not be {@literal null}.
|
||||
* @return the composed, repository-aware method lookup.
|
||||
* @see #direct()
|
||||
*/
|
||||
public static MethodLookup forRepositoryTypes(RepositoryMetadata repositoryMetadata) {
|
||||
return direct().and(new RepositoryAwareMethodLookup(repositoryMetadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository type-aware method lookup composed of {@link #direct()} and {@link ReactiveTypeInteropMethodLookup}.
|
||||
* <p/>
|
||||
* This method lookup considers adaptability of reactive types in method signatures. Repository methods accepting a
|
||||
* reactive type can be possibly called with a different reactive type if the reactive type can be adopted to the
|
||||
* target type. This lookup also permits assignable method signatures and resolves repository id/entity types but
|
||||
* prefers {@link #direct()} matches.
|
||||
*
|
||||
* @param repositoryMetadata must not be {@literal null}.
|
||||
* @return the composed, repository-aware method lookup.
|
||||
* @see #direct()
|
||||
* @see #forRepositoryTypes(RepositoryMetadata)
|
||||
*/
|
||||
public static MethodLookup forReactiveTypes(RepositoryMetadata repositoryMetadata) {
|
||||
return direct().and(new ReactiveTypeInteropMethodLookup(repositoryMetadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Default {@link MethodLookup} considering repository Id and entity types permitting calls to methods with assignable
|
||||
* arguments.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
private static class RepositoryAwareMethodLookup implements MethodLookup {
|
||||
|
||||
private static final TypeVariable<Class<Repository>>[] PARAMETERS = Repository.class.getTypeParameters();
|
||||
private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName();
|
||||
private static final String ID_TYPE_NAME = PARAMETERS[1].getName();
|
||||
|
||||
private final ResolvableType entityType;
|
||||
private final ResolvableType idType;
|
||||
private final Class<?> repositoryInterface;
|
||||
|
||||
public RepositoryAwareMethodLookup(RepositoryMetadata repositoryMetadata) {
|
||||
|
||||
this.entityType = ResolvableType.forClass(repositoryMetadata.getDomainType());
|
||||
this.idType = ResolvableType.forClass(repositoryMetadata.getIdType());
|
||||
this.repositoryInterface = repositoryMetadata.getRepositoryInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodPredicate> getLookups() {
|
||||
|
||||
MethodPredicate detailedComparison = (invoked, candidate) -> Optional.of(candidate)
|
||||
.filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name
|
||||
.filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount())
|
||||
.filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match
|
||||
.isPresent();
|
||||
|
||||
return Collections.singletonList(detailedComparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK}
|
||||
* is declared, the method ensures that given method parameter is the primary key type declared in the given
|
||||
* repository interface e.g.
|
||||
*
|
||||
* @param variable must not be {@literal null}.
|
||||
* @param parameterType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected boolean matchesGenericType(TypeVariable<?> variable, ResolvableType parameterType) {
|
||||
|
||||
GenericDeclaration declaration = variable.getGenericDeclaration();
|
||||
|
||||
if (declaration instanceof Class) {
|
||||
|
||||
if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Type boundType = variable.getBounds()[0];
|
||||
String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString();
|
||||
|
||||
return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType);
|
||||
}
|
||||
|
||||
for (Type type : variable.getBounds()) {
|
||||
if (ResolvableType.forType(type).isAssignableFrom(parameterType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
|
||||
* against the ones bound in the given repository interface.
|
||||
*
|
||||
* @param invokedMethod
|
||||
* @param candidate
|
||||
* @return
|
||||
*/
|
||||
private boolean parametersMatch(Method invokedMethod, Method candidate) {
|
||||
|
||||
Class<?>[] methodParameterTypes = invokedMethod.getParameterTypes();
|
||||
Type[] genericTypes = candidate.getGenericParameterTypes();
|
||||
Class<?>[] types = candidate.getParameterTypes();
|
||||
|
||||
for (int i = 0; i < genericTypes.length; i++) {
|
||||
|
||||
Type genericType = genericTypes[i];
|
||||
Class<?> type = types[i];
|
||||
MethodParameter parameter = new MethodParameter(invokedMethod, i);
|
||||
Class<?> parameterType = resolveParameterType(parameter, repositoryInterface);
|
||||
|
||||
if (genericType instanceof TypeVariable<?>) {
|
||||
|
||||
if (!matchesGenericType((TypeVariable<?>) genericType, ResolvableType.forMethodParameter(parameter))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (types[i].equals(parameterType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to {@link RepositoryAwareMethodLookup} considering reactive type adoption and entity types permitting
|
||||
* calls to methods with assignable arguments.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
private static class ReactiveTypeInteropMethodLookup extends RepositoryAwareMethodLookup {
|
||||
|
||||
private final RepositoryMetadata repositoryMetadata;
|
||||
|
||||
public ReactiveTypeInteropMethodLookup(RepositoryMetadata repositoryMetadata) {
|
||||
super(repositoryMetadata);
|
||||
this.repositoryMetadata = repositoryMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodPredicate> getLookups() {
|
||||
|
||||
MethodPredicate convertibleComparison = (invokedMethod, candidate) -> {
|
||||
|
||||
List<Supplier<Optional<Method>>> suppliers = new ArrayList<>();
|
||||
|
||||
if (usesParametersWithReactiveWrappers(invokedMethod.getMethod())) {
|
||||
suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, assignableWrapperMatch())); //
|
||||
suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, wrapperConversionMatch()));
|
||||
}
|
||||
|
||||
return suppliers.stream().anyMatch(supplier -> supplier.get().isPresent());
|
||||
};
|
||||
|
||||
MethodPredicate detailedComparison = (invokedMethod, candidate) -> getMethodCandidate(invokedMethod, candidate,
|
||||
matchParameterOrComponentType(repositoryMetadata.getRepositoryInterface())).isPresent();
|
||||
|
||||
return Arrays.asList(convertibleComparison, detailedComparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be
|
||||
* wrapped but supports unwrapping. Usually types like {@link Optional} or {@link java.util.stream.Stream}.
|
||||
*
|
||||
* @param repositoryInterface
|
||||
* @return
|
||||
* @see QueryExecutionConverters
|
||||
* @see #matchesGenericType
|
||||
*/
|
||||
private Predicate<ParameterOverrideCriteria> matchParameterOrComponentType(Class<?> repositoryInterface) {
|
||||
|
||||
return (parameterCriteria) -> {
|
||||
|
||||
Class<?> parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface);
|
||||
Type genericType = parameterCriteria.getGenericBaseType();
|
||||
|
||||
if (genericType instanceof TypeVariable<?>) {
|
||||
|
||||
if (!matchesGenericType((TypeVariable<?>) genericType,
|
||||
ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return parameterCriteria.getBaseType().isAssignableFrom(parameterType)
|
||||
&& parameterCriteria.isAssignableFromDeclared();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
|
||||
*
|
||||
* @param parameterType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean isNonUnwrappingWrapper(Class<?> parameterType) {
|
||||
|
||||
Assert.notNull(parameterType, "Parameter type must not be null!");
|
||||
|
||||
return QueryExecutionConverters.supports(parameterType)
|
||||
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link Method} uses a reactive wrapper type as parameter.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean usesParametersWithReactiveWrappers(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
return Arrays.stream(method.getParameterTypes())//
|
||||
.anyMatch(ReactiveTypeInteropMethodLookup::isNonUnwrappingWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a candidate method from the base class for the given one or the method given in the first place if none
|
||||
* one the base class matches.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param baseClass must not be {@literal null}.
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static Optional<Method> getMethodCandidate(InvokedMethod invokedMethod, Method candidate,
|
||||
Predicate<ParameterOverrideCriteria> predicate) {
|
||||
|
||||
return Optional.of(candidate)//
|
||||
.filter(it -> invokedMethod.getName().equals(it.getName()))//
|
||||
.filter(it -> invokedMethod.getParameterCount() == it.getParameterCount())//
|
||||
.filter(it -> parametersMatch(it, invokedMethod.getMethod(), predicate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
|
||||
* against the ones bound in the given repository interface.
|
||||
*
|
||||
* @param baseClassMethod must not be {@literal null}.
|
||||
* @param declaredMethod must not be {@literal null}.
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod,
|
||||
Predicate<ParameterOverrideCriteria> predicate) {
|
||||
|
||||
return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be
|
||||
* converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher}
|
||||
* conversion.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static Predicate<ParameterOverrideCriteria> wrapperConversionMatch() {
|
||||
|
||||
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
|
||||
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
|
||||
&& ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and
|
||||
* a declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher}
|
||||
* conversion.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static Predicate<ParameterOverrideCriteria> assignableWrapperMatch() {
|
||||
|
||||
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
|
||||
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
|
||||
&& parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType());
|
||||
}
|
||||
|
||||
private static Stream<ParameterOverrideCriteria> methodParameters(Method first, Method second) {
|
||||
|
||||
Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!");
|
||||
|
||||
return IntStream.range(0, first.getParameterCount()) //
|
||||
.mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index),
|
||||
new MethodParameter(second, index)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Criterion to represent {@link MethodParameter}s from a base method and its declared (overridden) method. Method
|
||||
* parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same
|
||||
* parameter count.
|
||||
*/
|
||||
@Value(staticConstructor = "of")
|
||||
static class ParameterOverrideCriteria {
|
||||
|
||||
private final MethodParameter base;
|
||||
private final MethodParameter declared;
|
||||
|
||||
/**
|
||||
* @return base method parameter type.
|
||||
*/
|
||||
public Class<?> getBaseType() {
|
||||
return base.getParameterType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return generic base method parameter type.
|
||||
*/
|
||||
public Type getGenericBaseType() {
|
||||
return base.getGenericParameterType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return declared method parameter type.
|
||||
*/
|
||||
public Class<?> getDeclaredType() {
|
||||
return declared.getParameterType();
|
||||
}
|
||||
|
||||
public boolean isAssignableFromDeclared() {
|
||||
return getBaseType().isAssignableFrom(getDeclaredType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,33 +15,9 @@
|
||||
*/
|
||||
package org.springframework.data.repository.core.support;
|
||||
|
||||
import static org.springframework.core.GenericTypeResolver.*;
|
||||
import static org.springframework.util.ReflectionUtils.*;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.data.repository.util.ReactiveWrapperConverters;
|
||||
import org.springframework.data.util.Optionals;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* This {@link RepositoryInformation} uses a {@link ConversionService} to check whether method arguments can be
|
||||
@@ -50,7 +26,9 @@ import org.springframework.util.Assert;
|
||||
* @author Mark Paluch
|
||||
* @author Oliver Gierke
|
||||
* @since 2.0
|
||||
* @deprecated as of 2.0, can be deleted...
|
||||
*/
|
||||
@Deprecated
|
||||
public class ReactiveRepositoryInformation extends DefaultRepositoryInformation {
|
||||
|
||||
/**
|
||||
@@ -59,209 +37,10 @@ public class ReactiveRepositoryInformation extends DefaultRepositoryInformation
|
||||
*
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param repositoryBaseClass must not be {@literal null}.
|
||||
* @param customImplementationClass can be {@literal null}.
|
||||
* @param composition can be {@literal null}.
|
||||
*/
|
||||
public ReactiveRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass,
|
||||
Optional<Class<?>> customImplementationClass) {
|
||||
super(metadata, repositoryBaseClass, customImplementationClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given target class' method if the given method (declared in the repository interface) was also declared
|
||||
* at the target class. Returns the given method if the given base class does not declare the method given. Takes
|
||||
* generics into account.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param baseClass must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
Method getTargetClassMethod(Method method, Optional<Class<?>> baseClass) {
|
||||
|
||||
Supplier<Optional<Method>> directMatch = () -> baseClass
|
||||
.map(it -> findMethod(it, method.getName(), method.getParameterTypes()));
|
||||
|
||||
Supplier<Optional<Method>> detailedComparison = () -> baseClass.flatMap(it -> {
|
||||
|
||||
List<Supplier<Optional<Method>>> suppliers = new ArrayList<>();
|
||||
|
||||
if (usesParametersWithReactiveWrappers(method)) {
|
||||
suppliers.add(() -> getMethodCandidate(method, it, assignableWrapperMatch())); //
|
||||
suppliers.add(() -> getMethodCandidate(method, it, wrapperConversionMatch()));
|
||||
}
|
||||
|
||||
suppliers.add(() -> getMethodCandidate(method, it, matchParameterOrComponentType(getRepositoryInterface())));
|
||||
|
||||
return Optionals.firstNonEmpty(Streamable.of(suppliers));
|
||||
});
|
||||
|
||||
return Optionals.firstNonEmpty(directMatch, detailedComparison).orElse(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be
|
||||
* wrapped but supports unwrapping. Usually types like {@link java.util.Optional} or {@link java.util.stream.Stream}.
|
||||
*
|
||||
* @param repositoryInterface
|
||||
* @return
|
||||
* @see QueryExecutionConverters
|
||||
* @see #matchesGenericType
|
||||
*/
|
||||
private Predicate<ParameterOverrideCriteria> matchParameterOrComponentType(Class<?> repositoryInterface) {
|
||||
|
||||
return (parameterCriteria) -> {
|
||||
|
||||
Class<?> parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface);
|
||||
Type genericType = parameterCriteria.getGenericBaseType();
|
||||
|
||||
if (genericType instanceof TypeVariable<?>) {
|
||||
|
||||
if (!matchesGenericType((TypeVariable<?>) genericType,
|
||||
ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return parameterCriteria.getBaseType().isAssignableFrom(parameterType)
|
||||
&& parameterCriteria.isAssignableFromDeclared();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
|
||||
*
|
||||
* @param parameterType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean isNonUnwrappingWrapper(Class<?> parameterType) {
|
||||
|
||||
Assert.notNull(parameterType, "Parameter type must not be null!");
|
||||
|
||||
return QueryExecutionConverters.supports(parameterType)
|
||||
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link Method} uses a reactive wrapper type as parameter.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean usesParametersWithReactiveWrappers(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
return Arrays.stream(method.getParameterTypes())//
|
||||
.anyMatch(ReactiveRepositoryInformation::isNonUnwrappingWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a candidate method from the base class for the given one or the method given in the first place if none one
|
||||
* the base class matches.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param baseClass must not be {@literal null}.
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static Optional<Method> getMethodCandidate(Method method, Class<?> baseClass,
|
||||
Predicate<ParameterOverrideCriteria> predicate) {
|
||||
|
||||
return Arrays.stream(baseClass.getMethods())//
|
||||
.filter(it -> method.getName().equals(it.getName()))//
|
||||
.filter(it -> method.getParameterCount() == it.getParameterCount())//
|
||||
.filter(it -> parametersMatch(it, method, predicate))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
|
||||
* against the ones bound in the given repository interface.
|
||||
*
|
||||
* @param baseClassMethod must not be {@literal null}.
|
||||
* @param declaredMethod must not be {@literal null}.
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod,
|
||||
Predicate<ParameterOverrideCriteria> predicate) {
|
||||
|
||||
return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be
|
||||
* converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher}
|
||||
* conversion.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static Predicate<ParameterOverrideCriteria> wrapperConversionMatch() {
|
||||
|
||||
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
|
||||
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
|
||||
&& ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and a
|
||||
* declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher}
|
||||
* conversion.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static Predicate<ParameterOverrideCriteria> assignableWrapperMatch() {
|
||||
|
||||
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
|
||||
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
|
||||
&& parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType());
|
||||
}
|
||||
|
||||
private static Stream<ParameterOverrideCriteria> methodParameters(Method first, Method second) {
|
||||
|
||||
Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!");
|
||||
|
||||
return IntStream.range(0, first.getParameterCount()) //
|
||||
.mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index),
|
||||
new MethodParameter(second, index)));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Criterion to represent {@link MethodParameter}s from a base method and its declared (overriden) method.
|
||||
* <p>
|
||||
* Method parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same
|
||||
* parameter count.
|
||||
*/
|
||||
@Value(staticConstructor = "of")
|
||||
private static class ParameterOverrideCriteria {
|
||||
|
||||
private final MethodParameter base;
|
||||
private final MethodParameter declared;
|
||||
|
||||
/**
|
||||
* @return base method parameter type.
|
||||
*/
|
||||
public Class<?> getBaseType() {
|
||||
return base.getParameterType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return generic base method parameter type.
|
||||
*/
|
||||
public Type getGenericBaseType() {
|
||||
return base.getGenericParameterType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return declared method parameter type.
|
||||
*/
|
||||
public Class<?> getDeclaredType() {
|
||||
return declared.getParameterType();
|
||||
}
|
||||
|
||||
public boolean isAssignableFromDeclared() {
|
||||
return getBaseType().isAssignableFrom(getDeclaredType());
|
||||
}
|
||||
RepositoryComposition composition) {
|
||||
super(metadata, repositoryBaseClass, composition.withMethodLookup(MethodLookups.forReactiveTypes(metadata)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* Copyright 2017 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.core.support;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod;
|
||||
import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Composite implementation to back repository method implementations.
|
||||
* <p />
|
||||
* A {@link RepositoryComposition} represents an ordered collection of {@link RepositoryFragment fragments}. Each
|
||||
* fragment contributes executable method signatures that are used by this composition to route method calls into the
|
||||
* according {@link RepositoryFragment}.
|
||||
* <p />
|
||||
* Fragments are allowed to contribute multiple implementations for a single method signature exposed through the
|
||||
* repository interface. {@link #withMethodLookup(MethodLookup) MethodLookup} selects the first matching method for
|
||||
* invocation. A composition also supports argument conversion between the repository method signature and fragment
|
||||
* implementation method through {@link #withArgumentConverter(BiFunction)}. Use argument conversion with a single
|
||||
* implementation method that can be exposed accepting convertible types.
|
||||
* <p />
|
||||
* Composition objects are immutable and thread-safe.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @soundtrack Masterboy - Anybody (Fj Gauder Mix)
|
||||
* @since 2.0
|
||||
* @see RepositoryFragment
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(of = "fragments")
|
||||
public class RepositoryComposition {
|
||||
|
||||
private static final BiFunction<Method, Object[], Object[]> PASSTHRU_ARG_CONVERTER = (methodParameter, o) -> o;
|
||||
|
||||
private static final RepositoryComposition EMPTY = new RepositoryComposition(RepositoryFragments.empty(),
|
||||
MethodLookups.direct(), PASSTHRU_ARG_CONVERTER);
|
||||
|
||||
private final Map<Method, Optional<Method>> methodCache = new ConcurrentReferenceHashMap<>();
|
||||
private final RepositoryFragments fragments;
|
||||
private final MethodLookup methodLookup;
|
||||
private final BiFunction<Method, Object[], Object[]> argumentConverter;
|
||||
|
||||
/**
|
||||
* Create an empty {@link RepositoryComposition}.
|
||||
*
|
||||
* @return an empty {@link RepositoryComposition}.
|
||||
*/
|
||||
public static RepositoryComposition empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RepositoryComposition} for just a single {@code implementation} with {@link MethodLookups#direct())
|
||||
* method lokup.
|
||||
*
|
||||
* @param implementation must not be {@literal null}.
|
||||
* @return the {@link RepositoryComposition} for a single {@code implementation}.
|
||||
*/
|
||||
public static RepositoryComposition just(Object implementation) {
|
||||
return new RepositoryComposition(RepositoryFragments.just(implementation), MethodLookups.direct(),
|
||||
PASSTHRU_ARG_CONVERTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with
|
||||
* {@link MethodLookups#direct()) method lokup.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}.
|
||||
*/
|
||||
public static RepositoryComposition of(RepositoryFragment<?>... fragments) {
|
||||
return of(Arrays.asList(fragments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with
|
||||
* {@link MethodLookups#direct()) method lokup.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}.
|
||||
*/
|
||||
public static RepositoryComposition of(List<RepositoryFragment<?>> fragments) {
|
||||
return new RepositoryComposition(RepositoryFragments.from(fragments), MethodLookups.direct(),
|
||||
PASSTHRU_ARG_CONVERTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RepositoryComposition} from {@link RepositoryFragments} and {@link RepositoryMetadata} with
|
||||
* {@link MethodLookups#direct()) method lokup.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the {@link RepositoryComposition} from {@link RepositoryFragments fragments}.
|
||||
*/
|
||||
public static RepositoryComposition of(RepositoryFragments fragments) {
|
||||
return new RepositoryComposition(fragments, MethodLookups.direct(), PASSTHRU_ARG_CONVERTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragment} to
|
||||
* the new composition. The resulting composition contains the appended {@link RepositoryFragment} as last element.
|
||||
*
|
||||
* @param fragment must not be {@literal null}.
|
||||
* @return the new {@link RepositoryComposition}.
|
||||
*/
|
||||
public RepositoryComposition append(RepositoryFragment<?> fragment) {
|
||||
return new RepositoryComposition(fragments.append(fragment), methodLookup, argumentConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragments}
|
||||
* to the new composition. The resulting composition contains the appended {@link RepositoryFragments} as last
|
||||
* element.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the new {@link RepositoryComposition}.
|
||||
*/
|
||||
public RepositoryComposition append(RepositoryFragments fragments) {
|
||||
return new RepositoryComposition(this.fragments.append(fragments), methodLookup, argumentConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RepositoryComposition} retaining current configuration and set {@code argumentConverter}.
|
||||
*
|
||||
* @param argumentConverter must not be {@literal null}.
|
||||
* @return the new {@link RepositoryComposition}.
|
||||
*/
|
||||
public RepositoryComposition withArgumentConverter(BiFunction<Method, Object[], Object[]> argumentConverter) {
|
||||
return new RepositoryComposition(fragments, methodLookup, argumentConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RepositoryComposition} retaining current configuration and set {@code methodLookup}.
|
||||
*
|
||||
* @param methodLookup must not be {@literal null}.
|
||||
* @return the new {@link RepositoryComposition}.
|
||||
*/
|
||||
public RepositoryComposition withMethodLookup(MethodLookup methodLookup) {
|
||||
return new RepositoryComposition(fragments, methodLookup, argumentConverter);
|
||||
}
|
||||
|
||||
public MethodLookup getMethodLookup() {
|
||||
return methodLookup;
|
||||
}
|
||||
|
||||
public RepositoryFragments getFragments() {
|
||||
return fragments;
|
||||
}
|
||||
|
||||
public BiFunction<Method, Object[], Object[]> getArgumentConverter() {
|
||||
return argumentConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}.
|
||||
*
|
||||
* @return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return fragments.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a method on the repository by routing the invocation to the appropriate {@link RepositoryFragment}.
|
||||
*
|
||||
* @param method
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
public Object invoke(Method method, Object... args) throws Throwable {
|
||||
|
||||
Method methodToCall = findMethod(method) //
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", method)));
|
||||
|
||||
ReflectionUtils.makeAccessible(methodToCall);
|
||||
|
||||
return fragments.invoke(methodToCall, argumentConverter.apply(methodToCall, args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the implementation method for the given {@link Method} invoked on the composite interface.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Optional<Method> findMethod(Method method) {
|
||||
|
||||
return methodCache.computeIfAbsent(method,
|
||||
key -> RepositoryFragments.findMethod(InvokedMethod.of(key), methodLookup, fragments::methods));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that all {@link RepositoryFragment fragments} have an implementation.
|
||||
*/
|
||||
public void validateImplementation() {
|
||||
|
||||
fragments.stream()
|
||||
.forEach(it -> it.getImplementation() //
|
||||
.orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
|
||||
ClassUtils.getQualifiedName(it.getSignatureContributor())))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing an ordered list of {@link RepositoryFragment fragments}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode
|
||||
public static class RepositoryFragments implements Streamable<RepositoryFragment<?>> {
|
||||
|
||||
static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList());
|
||||
|
||||
private final Map<Method, RepositoryFragment<?>> fragmentCache = new ConcurrentReferenceHashMap<>();
|
||||
private final List<RepositoryFragment<?>> fragments;
|
||||
|
||||
/**
|
||||
* Create empty {@link RepositoryFragments}.
|
||||
*
|
||||
* @return empty {@link RepositoryFragments}.
|
||||
*/
|
||||
public static RepositoryFragments empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link RepositoryFragments} from just implementation objects.
|
||||
*
|
||||
* @param implementations must not be {@literal null}.
|
||||
* @return the {@link RepositoryFragments} for {@code implementations}.
|
||||
*/
|
||||
public static RepositoryFragments just(Object... implementations) {
|
||||
|
||||
Assert.notNull(implementations, "Implementations must not be null!");
|
||||
Assert.noNullElements(implementations, "Implementations must not contain null elements!");
|
||||
|
||||
return new RepositoryFragments(
|
||||
Arrays.stream(implementations).map(RepositoryFragment::implemented).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link RepositoryFragments} from {@link RepositoryFragments fragments}.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the {@link RepositoryFragments} for {@code implementations}.
|
||||
*/
|
||||
public static RepositoryFragments of(RepositoryFragment<?>... fragments) {
|
||||
|
||||
Assert.notNull(fragments, "RepositoryFragments must not be null!");
|
||||
Assert.noNullElements(fragments, "RepositoryFragments must not contain null elements!");
|
||||
|
||||
return new RepositoryFragments(Arrays.asList(fragments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link RepositoryFragments} from a {@link List} of {@link RepositoryFragment fragments}.
|
||||
*
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return the {@link RepositoryFragments} for {@code implementations}.
|
||||
*/
|
||||
public static RepositoryFragments from(List<RepositoryFragment<?>> fragments) {
|
||||
|
||||
Assert.notNull(fragments, "RepositoryFragments must not be null!");
|
||||
|
||||
return new RepositoryFragments(new ArrayList<>(fragments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragment}.
|
||||
*
|
||||
* @param fragment must not be {@literal null}
|
||||
* @return the new {@link RepositoryFragments} containing all existing fragments and the given
|
||||
* {@link RepositoryFragment} as last element.
|
||||
*/
|
||||
public RepositoryFragments append(RepositoryFragment<?> fragment) {
|
||||
|
||||
Assert.notNull(fragment, "RepositoryFragment must not be null!");
|
||||
|
||||
return concat(stream(), Stream.of(fragment));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragments}.
|
||||
*
|
||||
* @param fragments must not be {@literal null}
|
||||
* @return the new {@link RepositoryFragments} containing all existing fragments and the given
|
||||
* {@link RepositoryFragments} as last elements.
|
||||
*/
|
||||
public RepositoryFragments append(RepositoryFragments fragments) {
|
||||
|
||||
Assert.notNull(fragments, "RepositoryFragments must not be null!");
|
||||
|
||||
return concat(stream(), fragments.stream());
|
||||
}
|
||||
|
||||
private static RepositoryFragments concat(Stream<RepositoryFragment<?>> left, Stream<RepositoryFragment<?>> right) {
|
||||
return from(Stream.concat(left, right).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@literal true} if this {@link RepositoryFragments} contains no {@link RepositoryFragment fragments}.
|
||||
*
|
||||
* @return {@literal true} if this {@link RepositoryFragments} contains no {@link RepositoryFragment fragments}.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return fragments.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RepositoryFragment<?>> iterator() {
|
||||
return fragments.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link Stream} of {@link RepositoryFragment fragments}.
|
||||
*/
|
||||
public Stream<RepositoryFragment<?>> stream() {
|
||||
return fragments.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link Stream} of {@link Method methods}.
|
||||
*/
|
||||
public Stream<Method> methods() {
|
||||
return stream().flatMap(RepositoryFragment::methods);
|
||||
}
|
||||
|
||||
static Optional<Method> findMethod(InvokedMethod invokedMethod, MethodLookup lookup,
|
||||
Supplier<Stream<Method>> methodStreamSupplier) {
|
||||
|
||||
for (MethodPredicate methodPredicate : lookup.getLookups()) {
|
||||
|
||||
Optional<Method> resolvedMethod = methodStreamSupplier.get()
|
||||
.filter(it -> methodPredicate.test(invokedMethod, it)) //
|
||||
.findFirst();
|
||||
|
||||
if (resolvedMethod.isPresent()) {
|
||||
return resolvedMethod;
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke {@link Method} by resolving the
|
||||
*
|
||||
* @param method
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
public Object invoke(Method method, Object[] args) throws Throwable {
|
||||
|
||||
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(method, key -> {
|
||||
|
||||
return stream().filter(it -> it.hasMethod(key)) //
|
||||
.filter(it -> it.getImplementation().isPresent()) //
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", key)));
|
||||
});
|
||||
|
||||
Object target = fragment.getImplementation().orElseThrow(
|
||||
() -> new IllegalArgumentException(String.format("No implementation found for method %s", method)));
|
||||
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return fragments.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2008-2016 the original author or authors.
|
||||
* Copyright 2008-2017 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.
|
||||
@@ -33,6 +33,7 @@ import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
|
||||
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
@@ -48,6 +49,7 @@ import org.springframework.util.Assert;
|
||||
* @param <T> the type of the repository
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID>
|
||||
implements InitializingBean, RepositoryFactoryInformation<S, ID>, FactoryBean<T>, BeanClassLoaderAware,
|
||||
@@ -59,6 +61,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
|
||||
private Key queryLookupStrategyKey;
|
||||
private Optional<Class<?>> repositoryBaseClass = Optional.empty();
|
||||
private Optional<Object> customImplementation = Optional.empty();
|
||||
private Optional<RepositoryFragments> repositoryFragments = Optional.empty();
|
||||
private NamedQueries namedQueries;
|
||||
private Optional<MappingContext<?, ?>> mappingContext;
|
||||
private ClassLoader classLoader;
|
||||
@@ -110,6 +113,15 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
|
||||
this.customImplementation = Optional.ofNullable(customImplementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to inject repository fragments.
|
||||
*
|
||||
* @param repositoryFragments
|
||||
*/
|
||||
public void setRepositoryFragments(RepositoryFragments repositoryFragments) {
|
||||
this.repositoryFragments = Optional.ofNullable(repositoryFragments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to inject a {@link NamedQueries} instance.
|
||||
*
|
||||
@@ -190,7 +202,8 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactoryInformation#getRepositoryInformation()
|
||||
*/
|
||||
public RepositoryInformation getRepositoryInformation() {
|
||||
return this.factory.getRepositoryInformation(repositoryMetadata, customImplementation.map(Object::getClass));
|
||||
return this.factory.getRepositoryInformation(repositoryMetadata,
|
||||
customImplementation.map(RepositoryComposition::just).orElse(RepositoryComposition.empty()));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -251,10 +264,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
|
||||
this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher));
|
||||
}
|
||||
|
||||
repositoryBaseClass.ifPresent(it -> this.factory.setRepositoryBaseClass(it));
|
||||
repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);
|
||||
|
||||
RepositoryFragments customImplementationFragment = customImplementation //
|
||||
.map(RepositoryFragments::just) //
|
||||
.orElseGet(RepositoryFragments::empty);
|
||||
|
||||
RepositoryFragments repositoryFragmentsToUse = this.repositoryFragments //
|
||||
.orElseGet(RepositoryFragments::empty) //
|
||||
.append(customImplementationFragment);
|
||||
|
||||
this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);
|
||||
this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, customImplementation));
|
||||
this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));
|
||||
|
||||
if (!lazyInit) {
|
||||
this.repository.get();
|
||||
|
||||
@@ -17,17 +17,19 @@ package org.springframework.data.repository.core.support;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
@@ -49,6 +51,7 @@ import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
|
||||
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
@@ -62,6 +65,8 @@ import org.springframework.data.util.Pair;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.transaction.interceptor.TransactionalProxy;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
|
||||
|
||||
/**
|
||||
* Factory bean to create instances of a given repository interface. Creates a proxy implementing the configured
|
||||
@@ -74,6 +79,37 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
|
||||
|
||||
private static final BiFunction<Method, Object[], Object[]> REACTIVE_ARGS_CONVERTER = (method, o) -> {
|
||||
|
||||
if (ReactiveWrappers.isAvailable()) {
|
||||
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
|
||||
Object[] converted = new Object[o.length];
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
|
||||
Class<?> parameterType = parameterTypes[i];
|
||||
Object value = o[i];
|
||||
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parameterType.isAssignableFrom(value.getClass())
|
||||
&& ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) {
|
||||
|
||||
converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType);
|
||||
} else {
|
||||
converted[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache;
|
||||
private final List<RepositoryProxyPostProcessor> postProcessors;
|
||||
|
||||
@@ -89,7 +125,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
|
||||
public RepositoryFactorySupport() {
|
||||
|
||||
this.repositoryInformationCache = new HashMap<>();
|
||||
this.repositoryInformationCache = new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
|
||||
this.postProcessors = new ArrayList<>();
|
||||
|
||||
this.repositoryBaseClass = Optional.empty();
|
||||
@@ -183,46 +219,82 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
this.postProcessors.add(processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add repository-specific extensions.
|
||||
*
|
||||
* @param metadata
|
||||
* @return
|
||||
*/
|
||||
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
|
||||
return RepositoryFragments.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling.
|
||||
*
|
||||
* @param metadata
|
||||
* @return
|
||||
*/
|
||||
protected RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) {
|
||||
|
||||
RepositoryComposition composition = RepositoryComposition.empty();
|
||||
|
||||
if (metadata.isReactiveRepository()) {
|
||||
return composition.withMethodLookup(MethodLookups.forReactiveTypes(metadata))
|
||||
.withArgumentConverter(REACTIVE_ARGS_CONVERTER);
|
||||
}
|
||||
|
||||
return composition.withMethodLookup(MethodLookups.forRepositoryTypes(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a repository instance for the given interface.
|
||||
*
|
||||
* @param <T>
|
||||
* @param repositoryInterface
|
||||
* @param repositoryInterface must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public <T> T getRepository(Class<T> repositoryInterface) {
|
||||
return getRepository(repositoryInterface, Optional.empty());
|
||||
return getRepository(repositoryInterface, RepositoryFragments.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a repository instance for the given interface backed by an instance providing implementation logic for
|
||||
* custom logic.
|
||||
*
|
||||
* @param <T>
|
||||
* @param repositoryInterface
|
||||
* @param customImplementation
|
||||
* @param repositoryInterface must not be {@literal null}.
|
||||
* @param customImplementation must not be {@literal null}.
|
||||
* @return
|
||||
* @deprecated since 2.0. Use {@link RepositoryFragments} with {@link #getRepository(Class, RepositoryFragments)} to
|
||||
* compose repositories backed by custom implementations.
|
||||
*/
|
||||
@Deprecated
|
||||
public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {
|
||||
return getRepository(repositoryInterface, Optional.of(customImplementation));
|
||||
return getRepository(repositoryInterface, RepositoryFragments.just(customImplementation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a repository instance for the given interface backed by an instance providing implementation logic for
|
||||
* custom logic.
|
||||
*
|
||||
* @param <T>
|
||||
* @param repositoryInterface
|
||||
* @param customImplementation
|
||||
* @param repositoryInterface must not be {@literal null}.
|
||||
* @param fragments must not be {@literal null}.
|
||||
* @return
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
protected <T> T getRepository(Class<T> repositoryInterface, Optional<Object> customImplementation) {
|
||||
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
|
||||
|
||||
Assert.notNull(repositoryInterface, "Repository interface must not be null!");
|
||||
Assert.notNull(fragments, "RepositoryFragments must not be null!");
|
||||
|
||||
RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
|
||||
RepositoryInformation information = getRepositoryInformation(metadata, customImplementation.map(Object::getClass));
|
||||
RepositoryComposition composition = getRepositoryComposition(metadata);
|
||||
RepositoryFragments repositoryAspects = getRepositoryFragments(metadata);
|
||||
|
||||
validate(information, customImplementation);
|
||||
RepositoryComposition compositionToUse = composition.append(fragments).append(repositoryAspects);
|
||||
RepositoryInformation information = getRepositoryInformation(metadata, compositionToUse);
|
||||
|
||||
validate(information, compositionToUse);
|
||||
|
||||
Object target = getTargetRepository(information);
|
||||
|
||||
@@ -239,9 +311,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
|
||||
result.addAdvice(new QueryExecutorMethodInterceptor(information));
|
||||
|
||||
result.addAdvice(information.isReactiveRepository()
|
||||
? new ConvertingImplementationMethodExecutionInterceptor(information, customImplementation, target)
|
||||
: new ImplementationMethodExecutionInterceptor(information, customImplementation, target));
|
||||
compositionToUse = compositionToUse.append(RepositoryFragment.implemented(target));
|
||||
result.addAdvice(new ImplementationMethodExecutionInterceptor(compositionToUse));
|
||||
|
||||
return (T) result.getProxy(classLoader);
|
||||
}
|
||||
@@ -260,21 +331,19 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
* Returns the {@link RepositoryInformation} for the given repository interface.
|
||||
*
|
||||
* @param metadata
|
||||
* @param customImplementationClass
|
||||
* @param composition
|
||||
* @return
|
||||
*/
|
||||
protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
|
||||
Optional<Class<?>> customImplementationClass) {
|
||||
RepositoryComposition composition) {
|
||||
|
||||
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, customImplementationClass);
|
||||
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);
|
||||
|
||||
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
|
||||
|
||||
Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));
|
||||
|
||||
return metadata.isReactiveRepository()
|
||||
? new ReactiveRepositoryInformation(metadata, baseClass, customImplementationClass)
|
||||
: new DefaultRepositoryInformation(metadata, baseClass, customImplementationClass);
|
||||
return new DefaultRepositoryInformation(metadata, baseClass, composition);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -302,7 +371,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
|
||||
/**
|
||||
* Returns the base class backing the actual repository instance. Make sure
|
||||
* {@link #getTargetRepository(RepositoryMetadata)} returns an instance of this class.
|
||||
* {@link #getTargetRepository(RepositoryInformation)} returns an instance of this class.
|
||||
*
|
||||
* @param metadata
|
||||
* @return
|
||||
@@ -326,20 +395,21 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
* Validates the given repository interface as well as the given custom implementation.
|
||||
*
|
||||
* @param repositoryInformation
|
||||
* @param customImplementation
|
||||
* @param composition
|
||||
*/
|
||||
private void validate(RepositoryInformation repositoryInformation, Optional<Object> customImplementation) {
|
||||
private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) {
|
||||
|
||||
customImplementation.orElseGet(() -> {
|
||||
if (repositoryInformation.hasCustomMethod()) {
|
||||
|
||||
if (!repositoryInformation.hasCustomMethod()) {
|
||||
return null;
|
||||
if (composition.isEmpty()) {
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("You have custom methods in %s but not provided a custom implementation!",
|
||||
repositoryInformation.getRepositoryInterface()));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("You have custom methods in %s but not provided a custom implementation!",
|
||||
repositoryInformation.getRepositoryInterface()));
|
||||
});
|
||||
composition.validateImplementation();
|
||||
}
|
||||
|
||||
validate(repositoryInformation);
|
||||
}
|
||||
@@ -356,11 +426,23 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
* @param constructorArguments
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final <R> R getTargetRepositoryViaReflection(RepositoryInformation information,
|
||||
Object... constructorArguments) {
|
||||
|
||||
Class<?> baseClass = information.getRepositoryBaseClass();
|
||||
return getTargetRepositoryViaReflection(baseClass, constructorArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a repository of the repository base class defined in the given {@link RepositoryInformation} using
|
||||
* reflection.
|
||||
*
|
||||
* @param baseClass
|
||||
* @param constructorArguments
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final <R> R getTargetRepositoryViaReflection(Class<?> baseClass, Object... constructorArguments) {
|
||||
Optional<Constructor<?>> constructor = ReflectionUtils.findConstructor(baseClass, constructorArguments);
|
||||
|
||||
return constructor.map(it -> (R) BeanUtils.instantiateClass(it, constructorArguments))
|
||||
@@ -467,16 +549,14 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
}
|
||||
|
||||
/**
|
||||
* Method interceptor that calls methods on either the base implementation or the custom repository implementation.
|
||||
* Method interceptor that calls methods on the {@link RepositoryComposition}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ImplementationMethodExecutionInterceptor implements MethodInterceptor {
|
||||
|
||||
private final RepositoryInformation repositoryInformation;
|
||||
private final Optional<Object> customImplementation;
|
||||
private final Object target;
|
||||
private final @NonNull RepositoryComposition composition;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
@@ -487,107 +567,14 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
Method method = invocation.getMethod();
|
||||
Object[] arguments = invocation.getArguments();
|
||||
|
||||
if (isCustomMethodInvocation(invocation)) {
|
||||
|
||||
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
|
||||
return executeMethodOn(customImplementation.get(), actualMethod, arguments);
|
||||
}
|
||||
|
||||
// Lookup actual method as it might be redeclared in the interface
|
||||
// and we have to use the repository instance nevertheless
|
||||
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
|
||||
return executeMethodOn(target, actualMethod, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given method on the given target. Correctly unwraps exceptions not caused by the reflection magic.
|
||||
*
|
||||
* @param target
|
||||
* @param method
|
||||
* @param parameters
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
|
||||
|
||||
try {
|
||||
return method.invoke(target, parameters);
|
||||
return composition.invoke(method, arguments);
|
||||
} catch (Exception e) {
|
||||
ClassUtils.unwrapReflectionException(e);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Should not occur!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link MethodInvocation} is considered to be targeted as an invocation of a custom
|
||||
* method.
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private boolean isCustomMethodInvocation(MethodInvocation invocation) {
|
||||
return customImplementation.map(it -> repositoryInformation.isCustomMethod(invocation.getMethod())).orElse(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method interceptor that converts parameters before invoking a method.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ConvertingImplementationMethodExecutionInterceptor extends ImplementationMethodExecutionInterceptor {
|
||||
|
||||
/**
|
||||
* @param repositoryInformation
|
||||
* @param customImplementation
|
||||
* @param target
|
||||
*/
|
||||
public ConvertingImplementationMethodExecutionInterceptor(RepositoryInformation repositoryInformation,
|
||||
Optional<Object> customImplementation, Object target) {
|
||||
|
||||
super(repositoryInformation, customImplementation, target);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport.ImplementationMethodExecutionInterceptor#executeMethodOn(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
|
||||
return super.executeMethodOn(target, method, convertParameters(method.getParameterTypes(), parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parameterTypes
|
||||
* @param parameters
|
||||
* @return
|
||||
*/
|
||||
private Object[] convertParameters(Class<?>[] parameterTypes, Object[] parameters) {
|
||||
|
||||
if (parameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Object[] result = new Object[parameters.length];
|
||||
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
|
||||
if (parameters[i] == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parameterTypes[i].isAssignableFrom(parameters[i].getClass()) && ReactiveWrappers.isAvailable()
|
||||
&& ReactiveWrapperConverters.canConvert(parameters[i].getClass(), parameterTypes[i])) {
|
||||
|
||||
result[i] = ReactiveWrapperConverters.toWrapper(parameters[i], parameterTypes[i]);
|
||||
} else {
|
||||
result[i] = parameters[i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -616,24 +603,25 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
* Simple value object to build up keys to cache {@link RepositoryInformation} instances.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Value
|
||||
private static class RepositoryInformationCacheKey {
|
||||
|
||||
private final String repositoryInterfaceName;
|
||||
private final String customImplementationClassName;
|
||||
String repositoryInterfaceName;
|
||||
final long compositionHash;
|
||||
|
||||
/**
|
||||
* Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and cuytom
|
||||
* implementation type.
|
||||
* Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and composition.
|
||||
*
|
||||
* @param repositoryInterfaceName must not be {@literal null}.
|
||||
* @param customImplementationClassName
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param composition must not be {@literal null}.
|
||||
*/
|
||||
public RepositoryInformationCacheKey(RepositoryMetadata metadata, Optional<Class<?>> customImplementationType) {
|
||||
public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) {
|
||||
|
||||
this.repositoryInterfaceName = metadata.getRepositoryInterface().getName();
|
||||
this.customImplementationClassName = customImplementationType.map(Class::getName).orElse(null);
|
||||
this.compositionHash = composition.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2017 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.core.support;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Value object representing a repository fragment.
|
||||
* <p />
|
||||
* Repository fragments are individual parts that contribute method signatures. They are used to form a
|
||||
* {@link RepositoryComposition}. Fragments can be purely structural or backed with an implementation.
|
||||
* <p/>
|
||||
* {@link #structural(Class) Structural} fragments are not backed by an implementation and are primarily used to
|
||||
* discover the structure of a repository composition and to perform validations.
|
||||
* <p/>
|
||||
* {@link #implemented(Object) Implemented} repository fragments consist of a signature contributor and the implementing
|
||||
* object. A signature contributor may be {@link #implemented(Class, Object) an interface} or
|
||||
* {@link #implemented(Object) just the implementing object} providing the available signatures for a repository.
|
||||
* <p/>
|
||||
* Fragments are immutable.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see RepositoryComposition
|
||||
*/
|
||||
public abstract class RepositoryFragment<T> {
|
||||
|
||||
/**
|
||||
* Create an implemented {@link RepositoryFragment} backed by the {@code implementation} object.
|
||||
*
|
||||
* @param implementation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static <T> RepositoryFragment<T> implemented(T implementation) {
|
||||
return new ImplementedRepositoryFragment<>(Optional.empty(), implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an implemented {@link RepositoryFragment} from a {@code interfaceClass} backed by the {@code implementation}
|
||||
* object.
|
||||
*
|
||||
* @param implementation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static <T> RepositoryFragment<T> implemented(Class<T> interfaceClass, T implementation) {
|
||||
return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a structural {@link RepositoryFragment} given {@code interfaceOrImplementation}.
|
||||
*
|
||||
* @param interfaceOrImplementation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static <T> RepositoryFragment<T> structural(Class<T> interfaceOrImplementation) {
|
||||
return new StructuralRepositoryFragment<>(interfaceOrImplementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find the {@link Method} by name and exact parameters. Returns {@literal true} if the method was found or
|
||||
* {@literal false} otherwise.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return {@literal true} if the method was found or {@literal false} otherwise
|
||||
*/
|
||||
public boolean hasMethod(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
return ReflectionUtils.findMethod(getSignatureContributor(), method.getName(), method.getParameterTypes()) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the class/interface providing signatures for this {@link RepositoryFragment}.
|
||||
*/
|
||||
protected abstract Class<?> getSignatureContributor();
|
||||
|
||||
/**
|
||||
* @return the optional implementation. Only available for implemented fragments. Structural fragments return always
|
||||
* {@link Optional#empty()}.
|
||||
*/
|
||||
public Optional<T> getImplementation() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement a structural {@link RepositoryFragment} given its {@code implementation} object. Returns an implemented
|
||||
* {@link RepositoryFragment}.
|
||||
*
|
||||
* @param implementation must not be {@literal null}.
|
||||
* @return a new implemented {@link RepositoryFragment} for {@code implementation}.
|
||||
*/
|
||||
public abstract RepositoryFragment<T> withImplementation(T implementation);
|
||||
|
||||
/**
|
||||
* @return a {@link Stream} of methods exposed by this {@link RepositoryFragment}.
|
||||
*/
|
||||
public Stream<Method> methods() {
|
||||
return Arrays.stream(getSignatureContributor().getMethods());
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
static class StructuralRepositoryFragment<T> extends RepositoryFragment<T> {
|
||||
|
||||
private final @NonNull Class<T> interfaceOrImplementation;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFragment#getSignatureContributor()
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getSignatureContributor() {
|
||||
return interfaceOrImplementation;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFragment#withImplementation(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public RepositoryFragment<T> withImplementation(T implementation) {
|
||||
return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceOrImplementation));
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
static class ImplementedRepositoryFragment<T> extends RepositoryFragment<T> {
|
||||
|
||||
private final @NonNull Optional<Class<T>> interfaceClass;
|
||||
private final @NonNull T implementation;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFragment#getSignatureContributor()
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public Class<?> getSignatureContributor() {
|
||||
return interfaceClass.orElse((Class) implementation.getClass());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFragment#getImplementation()
|
||||
*/
|
||||
@Override
|
||||
public Optional<T> getImplementation() {
|
||||
return Optional.of(implementation);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFragment#withImplementation(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public RepositoryFragment<T> withImplementation(T implementation) {
|
||||
return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return String.format("ImplementedRepositoryFragment %s%s",
|
||||
interfaceClass.map(ClassUtils::getShortName).map(it -> it + ":").orElse(""),
|
||||
ClassUtils.getShortName(implementation.getClass()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2017 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.core.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Factory bean for creation of {@link RepositoryFragments}. This {@link FactoryBean} uses named
|
||||
* {@link #RepositoryFragmentsFactoryBean(List) bean references} to look up {@link RepositoryFragment} beans and
|
||||
* construct {@link RepositoryFragments}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class RepositoryFragmentsFactoryBean<T>
|
||||
implements FactoryBean<RepositoryFragments>, BeanFactoryAware, InitializingBean {
|
||||
|
||||
private final List<String> fragmentBeanNames;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
private RepositoryFragments repositoryFragments = RepositoryFragments.empty();
|
||||
|
||||
/**
|
||||
* Creates a new {@link RepositoryFragmentsFactoryBean} given {@code fragmentBeanNames}.
|
||||
*
|
||||
* @param fragmentBeanNames must not be {@literal null}.
|
||||
*/
|
||||
public RepositoryFragmentsFactoryBean(List<String> fragmentBeanNames) {
|
||||
|
||||
Assert.notNull(fragmentBeanNames, "Fragment bean names must not be null!");
|
||||
this.fragmentBeanNames = fragmentBeanNames;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
List<RepositoryFragment<?>> fragments = (List) fragmentBeanNames.stream()
|
||||
.map(it -> beanFactory.getBean(it, RepositoryFragment.class)).collect(Collectors.toList());
|
||||
|
||||
this.repositoryFragments = RepositoryFragments.from(fragments);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.FactoryBean#getObject()
|
||||
*/
|
||||
@Override
|
||||
public RepositoryFragments getObject() throws Exception {
|
||||
return this.repositoryFragments;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.FactoryBean#getObjectType()
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return RepositoryComposition.class;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
|
||||
*/
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user