package org.springframework.data.repository.query;
import static org.springframework.data.repository.util.ClassUtils.*;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.stream.Stream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.EntityMetadata;
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.ClassTypeInformation;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
public class QueryMethod {
private final RepositoryMetadata metadata;
private final Method method;
private final Class<?> unwrappedReturnType;
private final Parameters<?, ?> parameters;
private final ResultProcessor resultProcessor;
private final Lazy<Class<?>> domainClass;
private final Lazy<Boolean> isCollectionQuery;
public QueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
Assert.notNull(method, "Method must not be null!");
Assert.notNull(metadata, "Repository metadata must not be null!");
Assert.notNull(factory, "ProjectionFactory must not be null!");
Parameters.TYPES.stream()
.filter(type -> getNumberOfOccurences(method, type) > 1)
.findFirst().ifPresent(type -> {
throw new IllegalStateException(
String.format("Method must only one argument of type %s! Offending method: %s", type.getSimpleName(),
method.toString()));
});
this.method = method;
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(metadata, method);
this.parameters = createParameters(method);
this.metadata = metadata;
if (hasParameterOfType(method, Pageable.class)) {
if (!isStreamQuery()) {
assertReturnTypeAssignable(method, QueryExecutionConverters.getAllowedPageableTypes());
}
if (hasParameterOfType(method, Sort.class)) {
throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. "
+ "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString()));
}
}
Assert.notNull(this.parameters,
() -> String.format("Parameters extracted from method '%s' must not be null!", method.getName()));
if (isPageQuery()) {
Assert.isTrue(this.parameters.hasPageableParameter(),
String.format("Paging query needs to have a Pageable parameter! Offending method %s", method.toString()));
}
this.domainClass = Lazy.of(() -> {
Class<?> repositoryDomainClass = metadata.getDomainType();
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
return repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass)
? methodDomainClass
: repositoryDomainClass;
});
this.resultProcessor = new ResultProcessor(this, factory);
this.isCollectionQuery = Lazy.of(this::calculateIsCollectionQuery);
}
protected Parameters<?, ?> createParameters(Method method) {
return new DefaultParameters(method);
}
public String getName() {
return method.getName();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public EntityMetadata<?> getEntityInformation() {
return () -> (Class) getDomainClass();
}
public String getNamedQueryName() {
return String.format("%s.%s", getDomainClass().getSimpleName(), method.getName());
}
protected Class<?> getDomainClass() {
return domainClass.get();
}
public Class<?> getReturnedObjectType() {
return metadata.getReturnedDomainClass(method);
}
public boolean isCollectionQuery() {
return isCollectionQuery.get();
}
public boolean isSliceQuery() {
return !isPageQuery() && org.springframework.util.ClassUtils.isAssignable(Slice.class, unwrappedReturnType);
}
public final boolean isPageQuery() {
return org.springframework.util.ClassUtils.isAssignable(Page.class, unwrappedReturnType);
}
public boolean isModifyingQuery() {
return false;
}
public boolean isQueryForEntity() {
return getDomainClass().isAssignableFrom(getReturnedObjectType());
}
public boolean isStreamQuery() {
return Stream.class.isAssignableFrom(unwrappedReturnType);
}
public Parameters<?, ?> getParameters() {
return parameters;
}
public ResultProcessor getResultProcessor() {
return resultProcessor;
}
@Override
public String toString() {
return method.toString();
}
private boolean calculateIsCollectionQuery() {
if (isPageQuery() || isSliceQuery()) {
return false;
}
Class<?> returnType = metadata.getReturnType(method).getType();
if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) {
return true;
}
if (QueryExecutionConverters.supports(unwrappedReturnType)) {
return !QueryExecutionConverters.isSingleValue(unwrappedReturnType);
}
return ClassTypeInformation.from(unwrappedReturnType).isCollectionLike();
}
private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
TypeInformation<?> returnType = metadata.getReturnType(method);
if (QueryExecutionConverters.supports(returnType.getType())
|| ReactiveWrapperConverters.supports(returnType.getType())) {
TypeInformation<?> componentType = returnType.getComponentType();
if (componentType == null) {
throw new IllegalStateException(
String.format("Couldn't find component type for return value of method %s!", method));
}
return componentType.getType();
}
return returnType.getType();
}
private static void assertReturnTypeAssignable(Method method, Set<Class<?>> types) {
Assert.notNull(method, "Method must not be null!");
Assert.notEmpty(types, "Types must not be null or empty!");
TypeInformation<?> returnType = ClassTypeInformation.fromReturnTypeOf(method);
returnType = QueryExecutionConverters.isSingleValue(returnType.getType())
? returnType.getRequiredComponentType()
: returnType;
for (Class<?> type : types) {
if (type.isAssignableFrom(returnType.getType())) {
return;
}
}
throw new IllegalStateException("Method has to have one of the following return types! " + types.toString());
}
}