/* * 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. * 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 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 * @author Christoph Strobl */ class DefaultRepositoryInformation implements RepositoryInformation { @SuppressWarnings("rawtypes") // private static final TypeVariable<Class<Repository>>[] PARAMETERS = Repository.class.getTypeParameters(); private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); private final Map<Method, Method> methodCache = new ConcurrentHashMap<>(); private final RepositoryMetadata metadata; private final Class<?> repositoryBaseClass; private final Optional<Class<?>> customImplementationClass; /** * 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}. */ public DefaultRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass, Optional<Class<?>> customImplementationClass) { 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!"); this.metadata = metadata; this.repositoryBaseClass = repositoryBaseClass; this.customImplementationClass = customImplementationClass; } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() */ @Override public Class<?> getDomainType() { return metadata.getDomainType(); } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() */ @Override public Class<?> getIdType() { return metadata.getIdType(); } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInformation#getRepositoryBaseClass() */ @Override public Class<?> getRepositoryBaseClass() { return this.repositoryBaseClass; } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInformation#getTargetClassMethod(java.lang.reflect.Method) */ @Override public Method getTargetClassMethod(Method method) { if (methodCache.containsKey(method)) { return methodCache.get(method); } Method result = getTargetClassMethod(method, customImplementationClass); if (!result.equals(method)) { return cacheAndReturn(method, result); } return cacheAndReturn(method, getTargetClassMethod(method, Optional.of(repositoryBaseClass))); } private Method cacheAndReturn(Method key, Method value) { if (value != null) { makeAccessible(value); } methodCache.put(key, value); return value; } /** * Returns whether the given method is considered to be a repository base class method. * * @param method * @return */ private boolean isTargetClassMethod(Method method, Optional<Class<?>> targetType) { Assert.notNull(method, "Method must not be null!"); return targetType.map(it -> method.getDeclaringClass().isAssignableFrom(it) || !method.equals(getTargetClassMethod(method, targetType))).orElse(false); } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInformation#getQueryMethods() */ @Override public Streamable<Method> getQueryMethods() { Set<Method> result = new HashSet<>(); for (Method method : getRepositoryInterface().getMethods()) { method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); if (isQueryMethodCandidate(method)) { result.add(method); } } return Streamable.of(Collections.unmodifiableSet(result)); } /** * Checks whether the given method is a query method candidate. * * @param method * @return */ private boolean isQueryMethodCandidate(Method method) { return !method.isBridge() && !method.isDefault() // && !Modifier.isStatic(method.getModifiers()) // && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); } /** * 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 */ private boolean isQueryAnnotationPresentOn(Method method) { return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInformation#isCustomMethod(java.lang.reflect.Method) */ @Override public boolean isCustomMethod(Method method) { return isTargetClassMethod(method, customImplementationClass); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryInformation#isQueryMethod(java.lang.reflect.Method) */ @Override public boolean isQueryMethod(Method method) { return getQueryMethods().stream().anyMatch(it -> it.equals(method)); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryInformation#isBaseClassMethod(java.lang.reflect.Method) */ @Override public boolean isBaseClassMethod(Method method) { Assert.notNull(method, "Method must not be null!"); return isTargetClassMethod(method, Optional.of(repositoryBaseClass)); } /** * Returns the given target class' method if the given method (declared in the repository interface) was also declared * at the target class. Returns the given method if the given base class does not declare the method given. Takes * generics into account. * * @param method must not be {@literal null} * @param baseClass * @return */ Method getTargetClassMethod(Method method, Optional<Class<?>> baseClass) { Supplier<Optional<Method>> directMatch = () -> baseClass .map(it -> findMethod(it, method.getName(), method.getParameterTypes())); Supplier<Optional<Method>> detailedComparison = () -> baseClass.flatMap(it -> Arrays.stream(it.getMethods())// .filter(baseClassMethod -> method.getName().equals(baseClassMethod.getName()))// Right name .filter(baseClassMethod -> method.getParameterCount() == baseClassMethod.getParameterCount()) .filter(baseClassMethod -> parametersMatch(method, baseClassMethod))// All parameters match .findFirst()); return Optionals.firstNonEmpty(directMatch, detailedComparison).orElse(method); } /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInformation#hasCustomMethod() */ @Override public boolean hasCustomMethod() { Class<?> repositoryInterface = getRepositoryInterface(); // No detection required if no typing interface was configured if (isGenericRepositoryInterface(repositoryInterface)) { return false; } for (Method method : repositoryInterface.getMethods()) { if (isCustomMethod(method) && !isBaseClassMethod(method)) { return true; } } return false; } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#getRepositoryInterface() */ @Override public Class<?> getRepositoryInterface() { return metadata.getRepositoryInterface(); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method) */ @Override public Class<?> getReturnedDomainClass(Method method) { return metadata.getReturnedDomainClass(method); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#getCrudMethods() */ @Override public CrudMethods getCrudMethods() { return metadata.getCrudMethods(); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#isPagingRepository() */ @Override public boolean isPagingRepository() { return metadata.isPagingRepository(); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#getAlternativeDomainTypes() */ @Override public Set<Class<?>> getAlternativeDomainTypes() { return metadata.getAlternativeDomainTypes(); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#isReactiveRepository() */ @Override 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; } }