DATACMNS-1026 - ExtensionAwareEvaluationContextProvider now returns all overloaded methods as functions.
All overloaded methods are now available in SPeL expressions. Among methods with identical argument list from different sources in the same extension (extension, root object, aliases) the last one in the order in parens wins. If there is more than one method for an application the following rules are applied: if there is one method with exact matching types in the argument list it is used, otherwise an exception is thrown. Original pull request: #217.
This commit is contained in:
committed by
Oliver Gierke
parent
5dba2f4c59
commit
42621086fe
@@ -29,14 +29,17 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter;
|
||||
import org.springframework.data.repository.query.Functions.NameAndArgumentCount;
|
||||
import org.springframework.data.repository.query.spi.EvaluationContextExtension;
|
||||
import org.springframework.data.repository.query.spi.Function;
|
||||
import org.springframework.data.util.MultiValueMapCollector;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.FieldFilter;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
@@ -126,7 +129,7 @@ class EvaluationContextExtensionInformation {
|
||||
*
|
||||
* @return the functions will never be {@literal null}.
|
||||
*/
|
||||
private final Map<String, Function> functions;
|
||||
private final MultiValueMap<NameAndArgumentCount, Function> functions;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ExtensionTypeInformation} fir the given type.
|
||||
@@ -141,15 +144,15 @@ class EvaluationContextExtensionInformation {
|
||||
this.properties = discoverDeclaredProperties(type);
|
||||
}
|
||||
|
||||
private static Map<String, Function> discoverDeclaredFunctions(Class<?> type) {
|
||||
private static MultiValueMap<NameAndArgumentCount, Function> discoverDeclaredFunctions(Class<?> type) {
|
||||
|
||||
Map<String, Function> map = new HashMap<>();
|
||||
MultiValueMap<NameAndArgumentCount, Function> map = CollectionUtils.toMultiValueMap(new HashMap<>());
|
||||
|
||||
ReflectionUtils.doWithMethods(type, //
|
||||
method -> map.put(method.getName(), new Function(method, null)), //
|
||||
method -> map.add(NameAndArgumentCount.of(method), new Function(method, null)), //
|
||||
PublicMethodAndFieldFilter.STATIC);
|
||||
|
||||
return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map);
|
||||
return CollectionUtils.unmodifiableMultiValueMap(map);
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -235,8 +238,7 @@ class EvaluationContextExtensionInformation {
|
||||
|
||||
}, PublicMethodAndFieldFilter.NON_STATIC);
|
||||
|
||||
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add,
|
||||
PublicMethodAndFieldFilter.NON_STATIC);
|
||||
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,14 +247,15 @@ class EvaluationContextExtensionInformation {
|
||||
* @param target can be {@literal null}.
|
||||
* @return the methods
|
||||
*/
|
||||
public Map<String, Function> getFunctions(Optional<Object> target) {
|
||||
public MultiValueMap<NameAndArgumentCount, Function> getFunctions(Optional<Object> target) {
|
||||
|
||||
return target.map(it -> methods.stream()//
|
||||
.collect(Collectors.toMap(//
|
||||
Method::getName, //
|
||||
method -> new Function(method, it), //
|
||||
(left, right) -> right)))
|
||||
.orElseGet(Collections::emptyMap);
|
||||
return target.map( //
|
||||
it -> methods.stream().collect( //
|
||||
new MultiValueMapCollector<>( //
|
||||
m -> NameAndArgumentCount.of(m), //
|
||||
m -> new Function(m, it) //
|
||||
))) //
|
||||
.orElseGet(() -> CollectionUtils.toMultiValueMap(Collections.emptyMap()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -60,6 +59,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
* @author Jens Schauder
|
||||
* @since 1.9
|
||||
*/
|
||||
public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware {
|
||||
@@ -316,10 +316,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
|
||||
*/
|
||||
private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAdapter adapter, String name,
|
||||
List<TypeDescriptor> argumentTypes) {
|
||||
|
||||
return adapter.getFunctions().entrySet().stream()//
|
||||
.filter(entry -> entry.getKey().equals(name))//
|
||||
.findFirst().map(Entry::getValue).map(FunctionMethodExecutor::new);
|
||||
return adapter.getFunctions().get(name, argumentTypes).map(FunctionMethodExecutor::new);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,7 +385,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
|
||||
|
||||
private final EvaluationContextExtension extension;
|
||||
|
||||
private final Map<String, Function> functions;
|
||||
private final Functions functions = new Functions();
|
||||
private final Map<String, Object> properties;
|
||||
|
||||
/**
|
||||
@@ -401,17 +398,16 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
|
||||
public EvaluationContextExtensionAdapter(EvaluationContextExtension extension,
|
||||
EvaluationContextExtensionInformation information) {
|
||||
|
||||
Assert.notNull(extension, "Extenstion must not be null!");
|
||||
Assert.notNull(extension, "Extension must not be null!");
|
||||
Assert.notNull(information, "Extension information must not be null!");
|
||||
|
||||
Optional<Object> target = Optional.ofNullable(extension.getRootObject());
|
||||
ExtensionTypeInformation extensionTypeInformation = information.getExtensionTypeInformation();
|
||||
RootObjectInformation rootObjectInformation = information.getRootObjectInformation(target);
|
||||
|
||||
this.functions = new HashMap<>();
|
||||
this.functions.putAll(extensionTypeInformation.getFunctions());
|
||||
this.functions.putAll(rootObjectInformation.getFunctions(target));
|
||||
this.functions.putAll(extension.getFunctions());
|
||||
functions.addAll(extension.getFunctions());
|
||||
functions.addAll(rootObjectInformation.getFunctions(target));
|
||||
functions.addAll(extensionTypeInformation.getFunctions());
|
||||
|
||||
this.properties = new HashMap<>();
|
||||
this.properties.putAll(extensionTypeInformation.getProperties());
|
||||
@@ -435,7 +431,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, Function> getFunctions() {
|
||||
Functions getFunctions() {
|
||||
return this.functions;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.query;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.data.repository.query.spi.Function;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* {@link MultiValueMap} like datastructure to keep lists of
|
||||
* {@link org.springframework.data.repository.query.spi.Function}s indexed by name and argument list length, where the
|
||||
* value lists are actually unique with respect to the signature.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 2.0
|
||||
*/
|
||||
class Functions {
|
||||
|
||||
private final MultiValueMap<NameAndArgumentCount, Function> functions = CollectionUtils
|
||||
.toMultiValueMap(new HashMap<>());
|
||||
|
||||
void addAll(Map<String, Function> newFunctions) {
|
||||
|
||||
newFunctions.forEach((n, f) -> {
|
||||
NameAndArgumentCount k = new NameAndArgumentCount(n, f.getParameterCount());
|
||||
List<Function> currentElements = get(k);
|
||||
if (!contains(currentElements, f)) {
|
||||
functions.add(k, f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void addAll(MultiValueMap<NameAndArgumentCount, Function> newFunctions) {
|
||||
|
||||
newFunctions.forEach((k, list) -> {
|
||||
List<Function> currentElements = get(k);
|
||||
list.stream() //
|
||||
.filter(f -> !contains(currentElements, f)) //
|
||||
.forEach(f -> functions.add(k, f));
|
||||
});
|
||||
}
|
||||
|
||||
List<Function> get(NameAndArgumentCount key) {
|
||||
return functions.getOrDefault(key, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function that best matches the parameters given. The {@code name} must match, and the
|
||||
* {@code argumentTypes} must be compatible with parameter list of the function. In order to resolve ambiguity it
|
||||
* checks for a method with exactly matching parameter list.
|
||||
*
|
||||
* @param name the name of the method
|
||||
* @param argumentTypes types of arguments that the method must be able to accept
|
||||
* @return a {@code Function} if a unique on gets found. {@code Optional.empty} if none matches. Throws
|
||||
* {@link IllegalStateException} if multiple functions match the parameters.
|
||||
*/
|
||||
Optional<Function> get(String name, List<TypeDescriptor> argumentTypes) {
|
||||
|
||||
Stream<Function> candidates = get(new NameAndArgumentCount(name, argumentTypes.size())).stream() //
|
||||
.filter(f -> f.supports(argumentTypes));
|
||||
return bestMatch(candidates.collect(Collectors.toList()), argumentTypes);
|
||||
}
|
||||
|
||||
private static boolean contains(List<Function> elements, Function f) {
|
||||
return elements.stream().anyMatch(f::isSignatureEqual);
|
||||
}
|
||||
|
||||
private static Optional<Function> bestMatch(List<Function> candidates, List<TypeDescriptor> argumentTypes) {
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (candidates.size() == 1) {
|
||||
return Optional.of(candidates.get(0));
|
||||
}
|
||||
|
||||
Optional<Function> exactMatch = candidates.stream().filter(f -> f.supportsExact(argumentTypes)).findFirst();
|
||||
if (!exactMatch.isPresent()) {
|
||||
throw new IllegalStateException(createErrorMessage(candidates, argumentTypes));
|
||||
}
|
||||
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
private static String createErrorMessage(List<Function> candidates, List<TypeDescriptor> argumentTypes) {
|
||||
|
||||
String argumentTypeString = String.join( //
|
||||
",", //
|
||||
argumentTypes.stream().map(TypeDescriptor::getName).collect(Collectors.toList()));
|
||||
|
||||
String messageTemplate = "There are multiple matching methods of name '%s' for parameter types (%s), but no "
|
||||
+ "exact match. Make sure to provide only one matching overload or one with exactly those types.";
|
||||
|
||||
return String.format(messageTemplate, candidates.get(0).getName(), argumentTypeString);
|
||||
}
|
||||
|
||||
@Value
|
||||
@AllArgsConstructor
|
||||
static class NameAndArgumentCount {
|
||||
String name;
|
||||
int count;
|
||||
|
||||
static NameAndArgumentCount of(Method m) {
|
||||
return new NameAndArgumentCount(m.getName(), m.getParameterCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2014-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.
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.repository.query.spi;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.util.TypeUtils;
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
* @author Jens Schauder
|
||||
* @since 1.9
|
||||
*/
|
||||
public class Function {
|
||||
@@ -115,4 +117,49 @@ public class Function {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of parameters required by the underlying method.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getParameterCount() {
|
||||
return method.getParameterCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the encapsulated method has exactly the argument types as those passed as an argument.
|
||||
*
|
||||
* @param argumentTypes a list of {@link TypeDescriptor}s to compare with the argument types of the method
|
||||
* @return {@code true} if the types are equal, {@code false} otherwise.
|
||||
*/
|
||||
public boolean supportsExact(List<TypeDescriptor> argumentTypes) {
|
||||
|
||||
if (method.getParameterCount() != argumentTypes.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
if (parameterTypes[i] != argumentTypes.get(i).getType()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether this {@code Function} has the same signature as another {@code Function}.
|
||||
*
|
||||
* @param other the {@code Function} to compare {@code this} with.
|
||||
*
|
||||
* @return {@code true} iff name and argument list are the same.
|
||||
*/
|
||||
public boolean isSignatureEqual(Function other) {
|
||||
|
||||
return getName().equals(other.getName()) //
|
||||
&& Arrays.equals(method.getParameterTypes(), other.method.getParameterTypes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* A {@link Collector} for building a {@link MultiValueMap} from a {@link java.util.stream.Stream}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 2.0
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class MultiValueMapCollector<T, K, V> implements Collector<T, MultiValueMap<K, V>, MultiValueMap<K, V>> {
|
||||
|
||||
@NonNull private final Function<T, K> keyFunction;
|
||||
@NonNull private final Function<T, V> valueFunction;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.stream.Collector#supplier()
|
||||
*/
|
||||
@Override
|
||||
public Supplier<MultiValueMap<K, V>> supplier() {
|
||||
return () -> CollectionUtils.toMultiValueMap(new HashMap<>());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.stream.Collector#accumulator()
|
||||
*/
|
||||
@Override
|
||||
public BiConsumer<MultiValueMap<K, V>, T> accumulator() {
|
||||
return (map, t) -> map.add(keyFunction.apply(t), valueFunction.apply(t));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.stream.Collector#combiner()
|
||||
*/
|
||||
@Override
|
||||
public BinaryOperator<MultiValueMap<K, V>> combiner() {
|
||||
|
||||
return (map1, map2) -> {
|
||||
|
||||
for (K key : map2.keySet()) {
|
||||
map1.addAll(key, map2.get(key));
|
||||
}
|
||||
|
||||
return map1;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.stream.Collector#finisher()
|
||||
*/
|
||||
@Override
|
||||
public Function<MultiValueMap<K, V>, MultiValueMap<K, V>> finisher() {
|
||||
return Function.identity();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.stream.Collector#characteristics()
|
||||
*/
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.UNORDERED);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Spring Data specific Java {@link Stream} utility methods and classes.
|
||||
@@ -77,4 +79,15 @@ public interface StreamUtils {
|
||||
public static <T> Collector<T, ?, Set<T>> toUnmodifiableSet() {
|
||||
return collectingAndThen(toSet(), Collections::unmodifiableSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Collector} to create a {@link MultiValueMap}.
|
||||
*
|
||||
* @param keyFunction {@link Function} to create a key from an element of the {@link java.util.stream.Stream}
|
||||
* @param valueFunction {@link Function} to create a value from an element of the {@link java.util.stream.Stream}
|
||||
*/
|
||||
public static <T, K, V> Collector<T, MultiValueMap<K, V>, MultiValueMap<K, V>> toMultiMap(Function<T, K> keyFunction,
|
||||
Function<T, V> valueFunction) {
|
||||
return new MultiValueMapCollector<T, K, V>(keyFunction, valueFunction);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user