diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index 9fb3ca4aa..8a5627b16 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -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 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; }); } -} \ No newline at end of file + + private List 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 detectRepositoryFragmentConfiguration( + RepositoryConfiguration configuration, String fragmentInterfaceName) { + + List exclusions = getExclusions(configuration); + + String className = ClassUtils.getShortName(fragmentInterfaceName) + .concat(configuration.getConfigurationSource().getRepositoryImplementationPostfix().orElse("Impl")); + + Optional 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 getExclusions(RepositoryConfiguration configuration) { + + List exclusions = new ArrayList<>(); + + for (TypeFilter typeFilter : configuration.getExcludeFilters()) { + exclusions.add(typeFilter); + } + + exclusions.add(new AnnotationTypeFilter(NoRepositoryBean.class)); + return exclusions; + } +} diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java index 5ba53355c..8fc5c24f7 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java @@ -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 { @@ -60,14 +61,18 @@ public interface RepositoryConfiguration 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"; + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index fd419ec63..88e963bb8 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -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>[] 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 methodCache = new ConcurrentHashMap<>(); private final RepositoryMetadata metadata; private final Class repositoryBaseClass; - private final Optional> 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> 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> 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> baseClass) { - - Supplier> directMatch = () -> baseClass - .map(it -> findMethod(it, method.getName(), method.getParameterTypes())); - - Supplier> 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; - } } diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java new file mode 100644 index 000000000..113b41714 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java @@ -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. + *

+ * {@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 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 { + + @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(); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java new file mode 100644 index 000000000..42c46fd72 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java @@ -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}. + *

+ * 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}. + *

+ * 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>[] 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 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 getLookups() { + + MethodPredicate convertibleComparison = (invokedMethod, candidate) -> { + + List>> 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 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 getMethodCandidate(InvokedMethod invokedMethod, Method candidate, + Predicate 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 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 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 assignableWrapperMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); + } + + private static Stream 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()); + } + } + } + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java index 95df89175..0e3373ac4 100644 --- a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java @@ -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> 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> baseClass) { - - Supplier> directMatch = () -> baseClass - .map(it -> findMethod(it, method.getName(), method.getParameterTypes())); - - Supplier> detailedComparison = () -> baseClass.flatMap(it -> { - - List>> 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 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 getMethodCandidate(Method method, Class baseClass, - Predicate 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 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 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 assignableWrapperMatch() { - - return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // - && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // - && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); - } - - private static Stream 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. - *

- * 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))); } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java new file mode 100644 index 000000000..f5e97e3e5 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -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. + *

+ * 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}. + *

+ * 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. + *

+ * 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 PASSTHRU_ARG_CONVERTER = (methodParameter, o) -> o; + + private static final RepositoryComposition EMPTY = new RepositoryComposition(RepositoryFragments.empty(), + MethodLookups.direct(), PASSTHRU_ARG_CONVERTER); + + private final Map> methodCache = new ConcurrentReferenceHashMap<>(); + private final RepositoryFragments fragments; + private final MethodLookup methodLookup; + private final BiFunction 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> 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 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 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 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> { + + static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList()); + + private final Map> fragmentCache = new ConcurrentReferenceHashMap<>(); + private final List> 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> 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> left, Stream> 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> iterator() { + return fragments.iterator(); + } + + /** + * @return {@link Stream} of {@link RepositoryFragment fragments}. + */ + public Stream> stream() { + return fragments.stream(); + } + + /** + * @return {@link Stream} of {@link Method methods}. + */ + public Stream methods() { + return stream().flatMap(RepositoryFragment::methods); + } + + static Optional findMethod(InvokedMethod invokedMethod, MethodLookup lookup, + Supplier> methodStreamSupplier) { + + for (MethodPredicate methodPredicate : lookup.getLookups()) { + + Optional 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(); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index b2b47b38b..8a967127b 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -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 the type of the repository * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ public abstract class RepositoryFactoryBeanSupport, S, ID> implements InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware, @@ -59,6 +61,7 @@ public abstract class RepositoryFactoryBeanSupport, private Key queryLookupStrategyKey; private Optional> repositoryBaseClass = Optional.empty(); private Optional customImplementation = Optional.empty(); + private Optional repositoryFragments = Optional.empty(); private NamedQueries namedQueries; private Optional> mappingContext; private ClassLoader classLoader; @@ -110,6 +113,15 @@ public abstract class RepositoryFactoryBeanSupport, 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, * @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, 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(); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index a496e7f7c..3a177ba0b 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -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 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 repositoryInformationCache; private final List 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 - * @param repositoryInterface + * @param repositoryInterface must not be {@literal null}. * @return */ public T getRepository(Class 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 - * @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 getRepository(Class 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 - * @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 getRepository(Class repositoryInterface, Optional customImplementation) { + public T getRepository(Class 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> 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 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 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 getTargetRepositoryViaReflection(Class baseClass, Object... constructorArguments) { Optional> 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 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 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> customImplementationType) { + public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) { this.repositoryInterfaceName = metadata.getRepositoryInterface().getName(); - this.customImplementationClassName = customImplementationType.map(Class::getName).orElse(null); + this.compositionHash = composition.hashCode(); } } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java new file mode 100644 index 000000000..c6ffb5bba --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java @@ -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. + *

+ * 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. + *

+ * {@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. + *

+ * {@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. + *

+ * Fragments are immutable. + * + * @author Mark Paluch + * @since 2.0 + * @see RepositoryComposition + */ +public abstract class RepositoryFragment { + + /** + * Create an implemented {@link RepositoryFragment} backed by the {@code implementation} object. + * + * @param implementation must not be {@literal null}. + * @return + */ + public static RepositoryFragment 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 RepositoryFragment implemented(Class 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 RepositoryFragment structural(Class 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 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 withImplementation(T implementation); + + /** + * @return a {@link Stream} of methods exposed by this {@link RepositoryFragment}. + */ + public Stream methods() { + return Arrays.stream(getSignatureContributor().getMethods()); + } + + @RequiredArgsConstructor + @EqualsAndHashCode(callSuper = false) + static class StructuralRepositoryFragment extends RepositoryFragment { + + private final @NonNull Class 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 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 extends RepositoryFragment { + + private final @NonNull Optional> 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 getImplementation() { + return Optional.of(implementation); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFragment#withImplementation(java.lang.Object) + */ + @Override + public RepositoryFragment 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())); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsFactoryBean.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsFactoryBean.java new file mode 100644 index 000000000..d53ae3369 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsFactoryBean.java @@ -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 + implements FactoryBean, BeanFactoryAware, InitializingBean { + + private final List 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 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> 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; + } +} diff --git a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java index 827221839..3f04ee9ee 100755 --- a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java @@ -38,6 +38,7 @@ import org.springframework.data.util.Streamable; * * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ public class AnnotationRepositoryConfigurationSourceUnitTests { @@ -62,14 +63,13 @@ public class AnnotationRepositoryConfigurationSourceUnitTests { .contains(AnnotationRepositoryConfigurationSourceUnitTests.class.getPackage().getName()); } - @Test // DATACMNS-47 + @Test // DATACMNS-47, DATACMNS-102 public void evaluatesExcludeFiltersCorrectly() { Streamable candidates = source.getCandidates(new DefaultResourceLoader()); - assertThat(candidates).hasSize(1); - BeanDefinition candidate = candidates.iterator().next(); - assertThat(candidate.getBeanClassName()).isEqualTo(MyRepository.class.getName()); + assertThat(candidates).hasSize(2).extracting("beanClassName").containsOnly(MyRepository.class.getName(), + ComposedRepository.class.getName()); } @Test // DATACMNS-47 diff --git a/src/test/java/org/springframework/data/repository/config/ComposedRepository.java b/src/test/java/org/springframework/data/repository/config/ComposedRepository.java new file mode 100644 index 000000000..31ac6683c --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/ComposedRepository.java @@ -0,0 +1,24 @@ +/* + * 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 org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSourceUnitTests.Person; + +/** + * @author Mark Paluch + */ +public interface ComposedRepository extends Repository, Mixin {} diff --git a/src/test/java/org/springframework/data/repository/config/Mixin.java b/src/test/java/org/springframework/data/repository/config/Mixin.java new file mode 100644 index 000000000..cc6a859e5 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/Mixin.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * @author Mark Paluch + */ +public interface Mixin { + + String getOne(); +} diff --git a/src/test/java/org/springframework/data/repository/config/MixinImpl.java b/src/test/java/org/springframework/data/repository/config/MixinImpl.java new file mode 100644 index 000000000..118f25913 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/MixinImpl.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * @author Mark Paluch + */ +public class MixinImpl implements Mixin { + + @Override + public String getOne() { + return "one"; + } +} diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java index f1926e306..d818fb901 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java @@ -29,9 +29,10 @@ import org.springframework.data.repository.config.RepositoryBeanDefinitionRegist /** * Integration tests for {@link RepositoryBeanDefinitionRegistrarSupport}. - * + * * @author Oliver Gierke * @author Peter Rietzler + * @author Mark Paluch */ public class RepositoryBeanDefinitionRegistrarSupportIntegrationTests { @@ -85,4 +86,9 @@ public class RepositoryBeanDefinitionRegistrarSupportIntegrationTests { public void registersExtensionAsBeanDefinition() { assertThat(context.getBean(DummyConfigurationExtension.class)).isNotNull(); } + + @Test // DATACMNS-102 + public void composedRepositoriesShouldBeAssembledCorrectly() { + assertThat(context.getBean(ComposedRepository.class).getOne()).isEqualTo("one"); + } } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java index e63c21330..2da112164 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.repository.config; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.lang.annotation.Annotation; @@ -37,6 +38,7 @@ import org.springframework.data.repository.core.support.DummyRepositoryFactoryBe * Integration test for {@link RepositoryBeanDefinitionRegistrarSupport}. * * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class RepositoryBeanDefinitionRegistrarSupportUnitTests { @@ -63,6 +65,9 @@ public class RepositoryBeanDefinitionRegistrarSupportUnitTests { registrar.registerBeanDefinitions(metadata, registry); assertBeanDefinitionRegisteredFor("myRepository"); + assertBeanDefinitionRegisteredFor("composedRepository"); + assertBeanDefinitionRegisteredFor("mixinImpl"); + assertBeanDefinitionRegisteredFor("mixinImplFragment"); assertNoBeanDefinitionRegisteredFor("profileRepository"); } @@ -100,7 +105,7 @@ public class RepositoryBeanDefinitionRegistrarSupportUnitTests { setResourceLoader(new DefaultResourceLoader()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() */ @@ -109,7 +114,7 @@ public class RepositoryBeanDefinitionRegistrarSupportUnitTests { return EnableRepositories.class; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() */ diff --git a/src/test/java/org/springframework/data/repository/core/support/DefaultCrudMethodsUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/DefaultCrudMethodsUnitTests.java index ab9804a86..fe671788b 100755 --- a/src/test/java/org/springframework/data/repository/core/support/DefaultCrudMethodsUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/DefaultCrudMethodsUnitTests.java @@ -38,6 +38,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; * * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ public class DefaultCrudMethodsUnitTests { @@ -143,7 +144,7 @@ public class DefaultCrudMethodsUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); RepositoryInformation information = new DefaultRepositoryInformation(metadata, PagingAndSortingRepository.class, - Optional.empty()); + RepositoryComposition.empty()); return new DefaultCrudMethods(information); } diff --git a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java index e04ce5cb7..d53f905b5 100755 --- a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-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. @@ -64,7 +64,8 @@ public class DefaultRepositoryInformationUnitTests { Method method = FooRepository.class.getMethod("findById", Integer.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); - DefaultRepositoryInformation information = new DefaultRepositoryInformation(metadata, REPOSITORY, Optional.empty()); + DefaultRepositoryInformation information = new DefaultRepositoryInformation(metadata, REPOSITORY, + RepositoryComposition.empty().withMethodLookup(MethodLookups.forRepositoryTypes(metadata))); Method reference = information.getTargetClassMethod(method); assertThat(reference.getDeclaringClass()).isEqualTo(REPOSITORY); @@ -78,7 +79,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); DefaultRepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty().withMethodLookup(MethodLookups.forRepositoryTypes(metadata))); assertThat(information.getTargetClassMethod(method)).isEqualTo(method); } @@ -88,7 +89,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation)); Method source = FooRepositoryCustom.class.getMethod("save", User.class); Method expected = customImplementation.getClass().getMethod("save", User.class); @@ -101,7 +102,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); assertThat(information.hasCustomMethod()).isFalse(); } @@ -111,7 +112,7 @@ public class DefaultRepositoryInformationUnitTests { DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomRepository.class); DefaultRepositoryInformation information = new DefaultRepositoryInformation(metadata, - PagingAndSortingRepository.class, Optional.empty()); + PagingAndSortingRepository.class, RepositoryComposition.empty()); Method method = CustomRepository.class.getMethod("findAll", Pageable.class); assertThat(information.isBaseClassMethod(method)).isTrue(); @@ -127,7 +128,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, PagingAndSortingRepository.class, - Optional.empty()); + RepositoryComposition.empty()); assertThat(information.getQueryMethods()).isEmpty(); } @@ -137,7 +138,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); Method saveMethod = BaseRepository.class.getMethod("save", Object.class); Method deleteMethod = BaseRepository.class.getMethod("delete", Object.class); @@ -152,7 +153,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); Method intermediateMethod = BaseRepository.class.getMethod("genericMethodToOverride", String.class); Method concreteMethod = ConcreteRepository.class.getMethod("genericMethodToOverride", String.class); @@ -168,7 +169,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); Method queryMethod = getMethodFrom(ConcreteRepository.class, "findBySomethingDifferent"); @@ -181,7 +182,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); Method method = BaseRepository.class.getMethod("findById", Object.class); @@ -193,7 +194,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(BossRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); Method method = BossRepository.class.getMethod("saveAll", Iterable.class); Method reference = CrudRepository.class.getMethod("saveAll", Iterable.class); @@ -206,7 +207,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomDefaultRepositoryMethodsRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.empty()); + RepositoryComposition.empty()); assertThat(information.getQueryMethods()).allMatch(method -> !method.isBridge()); } @@ -216,7 +217,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation)); Method source = FooRepositoryCustom.class.getMethod("exists", Object.class); Method expected = customImplementation.getClass().getMethod("exists", Object.class); @@ -230,7 +231,7 @@ public class DefaultRepositoryInformationUnitTests { GenericsSaveRepositoryImpl customImplementation = new GenericsSaveRepositoryImpl(); RepositoryMetadata metadata = new DefaultRepositoryMetadata(GenericsSaveRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, RepositoryFactorySupport.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation).withMethodLookup(MethodLookups.forRepositoryTypes(metadata))); Method customBaseRepositoryMethod = GenericsSaveRepository.class.getMethod("save", Object.class); assertThat(information.isCustomMethod(customBaseRepositoryMethod)).isTrue(); @@ -241,7 +242,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation)); Method method = FooRepository.class.getMethod("staticMethod"); @@ -253,7 +254,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation)); Method method = FooRepository.class.getMethod("defaultMethod"); @@ -265,7 +266,7 @@ public class DefaultRepositoryInformationUnitTests { RepositoryMetadata metadata = new DefaultRepositoryMetadata(DummyRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, DummyRepositoryImpl.class, - Optional.empty()); + RepositoryComposition.empty()); Method method = DummyRepository.class.getMethod("saveAll", Iterable.class); @@ -279,7 +280,7 @@ public class DefaultRepositoryInformationUnitTests { SimpleSaveRepositoryImpl customImplementation = new SimpleSaveRepositoryImpl(); RepositoryMetadata metadata = new DefaultRepositoryMetadata(SimpleSaveRepository.class); RepositoryInformation information = new DefaultRepositoryInformation(metadata, RepositoryFactorySupport.class, - Optional.of(customImplementation.getClass())); + RepositoryComposition.just(customImplementation).withMethodLookup(MethodLookups.forRepositoryTypes(metadata))); Method customBaseRepositoryMethod = SimpleSaveRepository.class.getMethod("save", Object.class); assertThat(information.isCustomMethod(customBaseRepositoryMethod)).isTrue(); diff --git a/src/test/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformationUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformationUnitTests.java index bc8fcab14..9c5d66e81 100644 --- a/src/test/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformationUnitTests.java @@ -24,14 +24,13 @@ import reactor.core.publisher.Flux; import rx.Observable; import java.lang.reflect.Method; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.data.repository.reactive.RxJava2CrudRepository; @@ -46,7 +45,7 @@ import org.springframework.data.repository.reactive.RxJava2CrudRepository; @RunWith(MockitoJUnitRunner.class) public class ReactiveRepositoryInformationUnitTests { - static final Class REPOSITORY = ReactiveJavaInterfaceWithGenerics.class; + static final Class BASE_CLASS = ReactiveJavaInterfaceWithGenerics.class; @Test // DATACMNS-836 public void discoversRxJava1MethodWithoutComparingReturnType() throws Exception { @@ -129,9 +128,12 @@ public class ReactiveRepositoryInformationUnitTests { private Method extractTargetMethodFromRepository(Class repositoryType, String methodName, Class... args) throws NoSuchMethodException { - RepositoryInformation information = new ReactiveRepositoryInformation(new DefaultRepositoryMetadata(repositoryType), - REPOSITORY, Optional.empty()); - return information.getTargetClassMethod(repositoryType.getMethod(methodName, args)); + RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryType); + + RepositoryComposition composition = RepositoryComposition.of(RepositoryFragment.structural(BASE_CLASS)) + .withMethodLookup(MethodLookups.forReactiveTypes(metadata)); + + return composition.findMethod(repositoryType.getMethod(methodName, args)).get(); } interface RxJava1InterfaceWithGenerics extends Repository { diff --git a/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java index 3c4e59855..bdd65fe7a 100644 --- a/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.repository.core.support; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import io.reactivex.Completable; diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java new file mode 100644 index 000000000..337866d87 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java @@ -0,0 +1,222 @@ +/* + * 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.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for {@link RepositoryComposition}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class RepositoryCompositionUnitTests { + + @Mock QueryByExampleExecutor queryByExampleExecutor; + + @Mock PersonRepository backingRepo; + + RepositoryComposition repositoryComposition; + + @Before + public void before() { + + RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( + new DefaultRepositoryMetadata(PersonRepository.class), backingRepo.getClass(), RepositoryComposition.empty()); + + RepositoryFragment mixin = RepositoryFragment.implemented(QueryByExampleExecutor.class, + queryByExampleExecutor); + + RepositoryFragment base = RepositoryFragment.implemented(backingRepo); + + repositoryComposition = RepositoryComposition.of(RepositoryFragments.of(mixin, base)) + .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)); + } + + @Test // DATACMNS-102 + public void shouldReportIfEmpty() { + + assertThat(RepositoryComposition.empty().isEmpty()).isTrue(); + assertThat(repositoryComposition.isEmpty()).isFalse(); + } + + @Test // DATACMNS-102 + public void shouldCallSaveOnBackingRepo() throws Throwable { + + Method save = ReflectionUtils.findMethod(PersonRepository.class, "save", Person.class); + + Method method = repositoryComposition.findMethod(save).get(); + + Person person = new Person(); + repositoryComposition.invoke(method, person); + + verify(backingRepo).save(person); + } + + @Test // DATACMNS-102 + public void shouldCallObjectSaveOnBackingRepo() throws Throwable { + + Method save = ReflectionUtils.findMethod(PersonRepository.class, "save", Object.class); + + Method method = repositoryComposition.findMethod(save).get(); + + Person person = new Person(); + repositoryComposition.invoke(method, person); + + verify(backingRepo).save((Object) person); + } + + @Test // DATACMNS-102 + public void shouldCallFindOneOnMixin() throws Throwable { + + Method findOne = ReflectionUtils.findMethod(PersonRepository.class, "findOne", Example.class); + + Method method = repositoryComposition.findMethod(findOne).get(); + + Person person = new Person(); + Example example = Example.of(person); + + repositoryComposition.invoke(method, example); + + verify(queryByExampleExecutor).findOne(example); + } + + @Test // DATACMNS-102 + public void shouldCallMethodsInOrder() throws Throwable { + + RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( + new DefaultRepositoryMetadata(OrderedRepository.class), OrderedRepository.class, RepositoryComposition.empty()); + + RepositoryFragment foo = RepositoryFragment.implemented(FooMixinImpl.INSTANCE); + RepositoryFragment bar = RepositoryFragment.implemented(BarMixinImpl.INSTANCE); + + RepositoryComposition fooBar = RepositoryComposition.of(RepositoryFragments.of(foo, bar)) + .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)); + + RepositoryComposition barFoo = RepositoryComposition.of(RepositoryFragments.of(bar, foo)) + .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)); + + Method getString = ReflectionUtils.findMethod(OrderedRepository.class, "getString"); + + assertThat(fooBar.invoke(fooBar.findMethod(getString).get())).isEqualTo("foo"); + + assertThat(barFoo.invoke(barFoo.findMethod(getString).get())).isEqualTo("bar"); + } + + @Test // DATACMNS-102 + public void shouldValidateStructuralFragments() { + + RepositoryComposition mixed = RepositoryComposition.of(RepositoryFragment.structural(QueryByExampleExecutor.class), + RepositoryFragment.implemented(backingRepo)); + + assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(mixed::validateImplementation) // + .withMessageContaining( + "Fragment org.springframework.data.repository.query.QueryByExampleExecutor has no implementation."); + } + + @Test // DATACMNS-102 + public void shouldValidateImplementationFragments() { + + RepositoryComposition mixed = RepositoryComposition.of(RepositoryFragment.implemented(backingRepo)); + + mixed.validateImplementation(); + } + + @Test // DATACMNS-102 + @SuppressWarnings("rawtypes") + public void shouldAppendCorrectly() { + + RepositoryFragment initial = RepositoryFragment.implemented(backingRepo); + RepositoryFragment structural = RepositoryFragment.structural(QueryByExampleExecutor.class); + + assertThat(RepositoryComposition.of(initial).append(structural).getFragments()).containsSequence(initial, + structural); + assertThat(RepositoryComposition.of(initial).append(RepositoryFragments.of(structural)).getFragments()) + .containsSequence(initial, structural); + } + + interface PersonRepository extends Repository, QueryByExampleExecutor { + + Person save(Person entity); + + Person save(Object entity); + + Person findOne(Person entity); + } + + @Data + static class Person { + + @Id String id; + } + + @Data + static class Contact { + + @Id String id; + } + + interface OrderedRepository extends Repository, FooMixin, BarMixin { + + } + + interface FooMixin { + + String getString(); + } + + enum FooMixinImpl implements FooMixin { + INSTANCE; + + @Override + public String getString() { + return "foo"; + } + } + + interface BarMixin { + + String getString(); + } + + enum BarMixinImpl implements BarMixin { + INSTANCE; + + @Override + public String getString() { + return "bar"; + } + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 71ed490d2..269dfa1d4 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-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. @@ -51,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.RepositoryQuery; import org.springframework.data.repository.sample.User; import org.springframework.data.util.Version; @@ -125,6 +126,17 @@ public class RepositoryFactorySupportUnitTests { verify(backingRepo, times(0)).findById(1); } + @Test // DATACMNS-102 + public void invokesCustomMethodCompositionMethodIfItRedeclaresACRUDOne() { + + ObjectRepository repository = factory.getRepository(ObjectRepository.class, + RepositoryFragments.just(customImplementation)); + repository.findById(1); + + verify(customImplementation, times(1)).findById(1); + verify(backingRepo, times(0)).findById(1); + } + @Test public void createsRepositoryInstanceWithCustomIntermediateRepository() {